2015-05-10 12:35:09 +00:00
|
|
|
|
#!/usr/bin/env python3
|
2017-05-22 16:41:03 +00:00
|
|
|
|
# -*- coding: utf-8 -*-
|
2014-07-26 14:12:40 +00:00
|
|
|
|
|
2021-05-07 07:41:27 +00:00
|
|
|
|
# Copyright (C) 2012-2021 Johannes Schauer Marin Rodrigues <josch@mister-muffin.de>
|
2013-05-02 06:17:13 +00:00
|
|
|
|
#
|
2014-03-30 06:10:12 +00:00
|
|
|
|
# This program is free software: you can redistribute it and/or
|
|
|
|
|
# modify it under the terms of the GNU Lesser General Public
|
|
|
|
|
# License as published by the Free Software Foundation, either
|
|
|
|
|
# version 3 of the License, or (at your option) any later
|
|
|
|
|
# version.
|
2013-05-02 06:17:13 +00:00
|
|
|
|
#
|
|
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
|
# GNU General Public License for more details.
|
|
|
|
|
#
|
2014-03-30 06:10:12 +00:00
|
|
|
|
# You should have received a copy of the GNU General Public
|
|
|
|
|
# License along with this program. If not, see
|
|
|
|
|
# <http://www.gnu.org/licenses/>.
|
2012-03-29 09:08:32 +00:00
|
|
|
|
|
|
|
|
|
import sys
|
2016-02-14 17:54:59 +00:00
|
|
|
|
import os
|
2012-03-29 09:08:32 +00:00
|
|
|
|
import zlib
|
|
|
|
|
import argparse
|
2018-08-01 20:12:40 +00:00
|
|
|
|
from PIL import Image, TiffImagePlugin
|
2018-12-25 11:30:10 +00:00
|
|
|
|
|
|
|
|
|
# TiffImagePlugin.DEBUG = True
|
2018-12-25 00:45:38 +00:00
|
|
|
|
from PIL.ExifTags import TAGS
|
2012-03-29 09:08:32 +00:00
|
|
|
|
from datetime import datetime
|
2013-10-21 13:55:47 +00:00
|
|
|
|
from jp2 import parsejp2
|
2015-05-10 12:35:09 +00:00
|
|
|
|
from enum import Enum
|
|
|
|
|
from io import BytesIO
|
|
|
|
|
import logging
|
2018-03-15 10:31:36 +00:00
|
|
|
|
import struct
|
2019-08-25 04:03:48 +00:00
|
|
|
|
import platform
|
2020-06-01 00:10:07 +00:00
|
|
|
|
import hashlib
|
2020-09-23 16:39:06 +00:00
|
|
|
|
from itertools import chain
|
2015-05-10 12:35:09 +00:00
|
|
|
|
|
2021-04-12 12:12:14 +00:00
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
2020-05-28 10:46:37 +00:00
|
|
|
|
have_pdfrw = True
|
2020-04-23 04:59:07 +00:00
|
|
|
|
try:
|
|
|
|
|
import pdfrw
|
|
|
|
|
except ImportError:
|
2020-05-28 10:46:37 +00:00
|
|
|
|
have_pdfrw = False
|
2020-04-23 04:59:07 +00:00
|
|
|
|
|
2020-05-31 15:18:02 +00:00
|
|
|
|
have_pikepdf = True
|
|
|
|
|
try:
|
|
|
|
|
import pikepdf
|
|
|
|
|
except ImportError:
|
|
|
|
|
have_pikepdf = False
|
|
|
|
|
|
2021-05-09 16:57:41 +00:00
|
|
|
|
__version__ = "0.4.1"
|
2015-05-10 12:35:09 +00:00
|
|
|
|
default_dpi = 96.0
|
|
|
|
|
papersizes = {
|
|
|
|
|
"letter": "8.5inx11in",
|
2018-12-25 11:30:10 +00:00
|
|
|
|
"a0": "841mmx1189mm",
|
|
|
|
|
"a1": "594mmx841mm",
|
|
|
|
|
"a2": "420mmx594mm",
|
|
|
|
|
"a3": "297mmx420mm",
|
|
|
|
|
"a4": "210mmx297mm",
|
|
|
|
|
"a5": "148mmx210mm",
|
|
|
|
|
"a6": "105mmx148mm",
|
|
|
|
|
"legal": "8.5inx14in",
|
2018-12-24 17:02:35 +00:00
|
|
|
|
"tabloid": "11inx17in",
|
2015-05-10 12:35:09 +00:00
|
|
|
|
}
|
|
|
|
|
papernames = {
|
|
|
|
|
"letter": "Letter",
|
2018-12-25 11:30:10 +00:00
|
|
|
|
"a0": "A0",
|
|
|
|
|
"a1": "A1",
|
|
|
|
|
"a2": "A2",
|
|
|
|
|
"a3": "A3",
|
|
|
|
|
"a4": "A4",
|
|
|
|
|
"a5": "A5",
|
|
|
|
|
"a6": "A6",
|
|
|
|
|
"legal": "Legal",
|
2018-12-24 17:02:35 +00:00
|
|
|
|
"tabloid": "Tabloid",
|
2015-05-10 12:35:09 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-05-28 10:46:37 +00:00
|
|
|
|
Engine = Enum("Engine", "internal pdfrw pikepdf")
|
2015-05-10 12:35:09 +00:00
|
|
|
|
|
2021-05-14 09:30:23 +00:00
|
|
|
|
Rotation = Enum("Rotation", "auto none ifvalid 0 90 180 270")
|
|
|
|
|
|
2018-12-25 11:30:10 +00:00
|
|
|
|
FitMode = Enum("FitMode", "into fill exact shrink enlarge")
|
2015-05-10 12:35:09 +00:00
|
|
|
|
|
2018-12-25 11:30:10 +00:00
|
|
|
|
PageOrientation = Enum("PageOrientation", "portrait landscape")
|
2015-05-10 12:35:09 +00:00
|
|
|
|
|
2018-12-25 11:30:10 +00:00
|
|
|
|
Colorspace = Enum("Colorspace", "RGB L 1 CMYK CMYK;I RGBA P other")
|
2015-05-10 12:35:09 +00:00
|
|
|
|
|
2021-04-12 20:57:51 +00:00
|
|
|
|
ImageFormat = Enum("ImageFormat", "JPEG JPEG2000 CCITTGroup4 PNG TIFF MPO other")
|
2015-05-10 12:35:09 +00:00
|
|
|
|
|
2018-12-25 11:30:10 +00:00
|
|
|
|
PageMode = Enum("PageMode", "none outlines thumbs")
|
2012-03-29 09:08:32 +00:00
|
|
|
|
|
2018-12-25 11:30:10 +00:00
|
|
|
|
PageLayout = Enum("PageLayout", "single onecolumn twocolumnright twocolumnleft")
|
2014-03-01 04:51:53 +00:00
|
|
|
|
|
2018-12-25 11:30:10 +00:00
|
|
|
|
Magnification = Enum("Magnification", "fit fith fitbh")
|
2014-03-01 04:51:53 +00:00
|
|
|
|
|
2018-12-25 11:30:10 +00:00
|
|
|
|
ImgSize = Enum("ImgSize", "abs perc dpi")
|
2015-05-10 12:35:09 +00:00
|
|
|
|
|
2018-12-25 11:30:10 +00:00
|
|
|
|
Unit = Enum("Unit", "pt cm mm inch")
|
2015-05-10 12:35:09 +00:00
|
|
|
|
|
2018-12-25 11:30:10 +00:00
|
|
|
|
ImgUnit = Enum("ImgUnit", "pt cm mm inch perc dpi")
|
2015-05-10 12:35:09 +00:00
|
|
|
|
|
2018-08-15 22:10:46 +00:00
|
|
|
|
TIFFBitRevTable = [
|
2018-12-25 11:30:10 +00:00
|
|
|
|
0x00,
|
|
|
|
|
0x80,
|
|
|
|
|
0x40,
|
|
|
|
|
0xC0,
|
|
|
|
|
0x20,
|
|
|
|
|
0xA0,
|
|
|
|
|
0x60,
|
|
|
|
|
0xE0,
|
|
|
|
|
0x10,
|
|
|
|
|
0x90,
|
|
|
|
|
0x50,
|
|
|
|
|
0xD0,
|
|
|
|
|
0x30,
|
|
|
|
|
0xB0,
|
|
|
|
|
0x70,
|
|
|
|
|
0xF0,
|
|
|
|
|
0x08,
|
|
|
|
|
0x88,
|
|
|
|
|
0x48,
|
|
|
|
|
0xC8,
|
|
|
|
|
0x28,
|
|
|
|
|
0xA8,
|
|
|
|
|
0x68,
|
|
|
|
|
0xE8,
|
|
|
|
|
0x18,
|
|
|
|
|
0x98,
|
|
|
|
|
0x58,
|
|
|
|
|
0xD8,
|
|
|
|
|
0x38,
|
|
|
|
|
0xB8,
|
|
|
|
|
0x78,
|
|
|
|
|
0xF8,
|
|
|
|
|
0x04,
|
|
|
|
|
0x84,
|
|
|
|
|
0x44,
|
|
|
|
|
0xC4,
|
|
|
|
|
0x24,
|
|
|
|
|
0xA4,
|
|
|
|
|
0x64,
|
|
|
|
|
0xE4,
|
|
|
|
|
0x14,
|
|
|
|
|
0x94,
|
|
|
|
|
0x54,
|
|
|
|
|
0xD4,
|
|
|
|
|
0x34,
|
|
|
|
|
0xB4,
|
|
|
|
|
0x74,
|
|
|
|
|
0xF4,
|
|
|
|
|
0x0C,
|
|
|
|
|
0x8C,
|
|
|
|
|
0x4C,
|
|
|
|
|
0xCC,
|
|
|
|
|
0x2C,
|
|
|
|
|
0xAC,
|
|
|
|
|
0x6C,
|
|
|
|
|
0xEC,
|
|
|
|
|
0x1C,
|
|
|
|
|
0x9C,
|
|
|
|
|
0x5C,
|
|
|
|
|
0xDC,
|
|
|
|
|
0x3C,
|
|
|
|
|
0xBC,
|
|
|
|
|
0x7C,
|
|
|
|
|
0xFC,
|
|
|
|
|
0x02,
|
|
|
|
|
0x82,
|
|
|
|
|
0x42,
|
|
|
|
|
0xC2,
|
|
|
|
|
0x22,
|
|
|
|
|
0xA2,
|
|
|
|
|
0x62,
|
|
|
|
|
0xE2,
|
|
|
|
|
0x12,
|
|
|
|
|
0x92,
|
|
|
|
|
0x52,
|
|
|
|
|
0xD2,
|
|
|
|
|
0x32,
|
|
|
|
|
0xB2,
|
|
|
|
|
0x72,
|
|
|
|
|
0xF2,
|
|
|
|
|
0x0A,
|
|
|
|
|
0x8A,
|
|
|
|
|
0x4A,
|
|
|
|
|
0xCA,
|
|
|
|
|
0x2A,
|
|
|
|
|
0xAA,
|
|
|
|
|
0x6A,
|
|
|
|
|
0xEA,
|
|
|
|
|
0x1A,
|
|
|
|
|
0x9A,
|
|
|
|
|
0x5A,
|
|
|
|
|
0xDA,
|
|
|
|
|
0x3A,
|
|
|
|
|
0xBA,
|
|
|
|
|
0x7A,
|
|
|
|
|
0xFA,
|
|
|
|
|
0x06,
|
|
|
|
|
0x86,
|
|
|
|
|
0x46,
|
|
|
|
|
0xC6,
|
|
|
|
|
0x26,
|
|
|
|
|
0xA6,
|
|
|
|
|
0x66,
|
|
|
|
|
0xE6,
|
|
|
|
|
0x16,
|
|
|
|
|
0x96,
|
|
|
|
|
0x56,
|
|
|
|
|
0xD6,
|
|
|
|
|
0x36,
|
|
|
|
|
0xB6,
|
|
|
|
|
0x76,
|
|
|
|
|
0xF6,
|
|
|
|
|
0x0E,
|
|
|
|
|
0x8E,
|
|
|
|
|
0x4E,
|
|
|
|
|
0xCE,
|
|
|
|
|
0x2E,
|
|
|
|
|
0xAE,
|
|
|
|
|
0x6E,
|
|
|
|
|
0xEE,
|
|
|
|
|
0x1E,
|
|
|
|
|
0x9E,
|
|
|
|
|
0x5E,
|
|
|
|
|
0xDE,
|
|
|
|
|
0x3E,
|
|
|
|
|
0xBE,
|
|
|
|
|
0x7E,
|
|
|
|
|
0xFE,
|
|
|
|
|
0x01,
|
|
|
|
|
0x81,
|
|
|
|
|
0x41,
|
|
|
|
|
0xC1,
|
|
|
|
|
0x21,
|
|
|
|
|
0xA1,
|
|
|
|
|
0x61,
|
|
|
|
|
0xE1,
|
|
|
|
|
0x11,
|
|
|
|
|
0x91,
|
|
|
|
|
0x51,
|
|
|
|
|
0xD1,
|
|
|
|
|
0x31,
|
|
|
|
|
0xB1,
|
|
|
|
|
0x71,
|
|
|
|
|
0xF1,
|
|
|
|
|
0x09,
|
|
|
|
|
0x89,
|
|
|
|
|
0x49,
|
|
|
|
|
0xC9,
|
|
|
|
|
0x29,
|
|
|
|
|
0xA9,
|
|
|
|
|
0x69,
|
|
|
|
|
0xE9,
|
|
|
|
|
0x19,
|
|
|
|
|
0x99,
|
|
|
|
|
0x59,
|
|
|
|
|
0xD9,
|
|
|
|
|
0x39,
|
|
|
|
|
0xB9,
|
|
|
|
|
0x79,
|
|
|
|
|
0xF9,
|
|
|
|
|
0x05,
|
|
|
|
|
0x85,
|
|
|
|
|
0x45,
|
|
|
|
|
0xC5,
|
|
|
|
|
0x25,
|
|
|
|
|
0xA5,
|
|
|
|
|
0x65,
|
|
|
|
|
0xE5,
|
|
|
|
|
0x15,
|
|
|
|
|
0x95,
|
|
|
|
|
0x55,
|
|
|
|
|
0xD5,
|
|
|
|
|
0x35,
|
|
|
|
|
0xB5,
|
|
|
|
|
0x75,
|
|
|
|
|
0xF5,
|
|
|
|
|
0x0D,
|
|
|
|
|
0x8D,
|
|
|
|
|
0x4D,
|
|
|
|
|
0xCD,
|
|
|
|
|
0x2D,
|
|
|
|
|
0xAD,
|
|
|
|
|
0x6D,
|
|
|
|
|
0xED,
|
|
|
|
|
0x1D,
|
|
|
|
|
0x9D,
|
|
|
|
|
0x5D,
|
|
|
|
|
0xDD,
|
|
|
|
|
0x3D,
|
|
|
|
|
0xBD,
|
|
|
|
|
0x7D,
|
|
|
|
|
0xFD,
|
|
|
|
|
0x03,
|
|
|
|
|
0x83,
|
|
|
|
|
0x43,
|
|
|
|
|
0xC3,
|
|
|
|
|
0x23,
|
|
|
|
|
0xA3,
|
|
|
|
|
0x63,
|
|
|
|
|
0xE3,
|
|
|
|
|
0x13,
|
|
|
|
|
0x93,
|
|
|
|
|
0x53,
|
|
|
|
|
0xD3,
|
|
|
|
|
0x33,
|
|
|
|
|
0xB3,
|
|
|
|
|
0x73,
|
|
|
|
|
0xF3,
|
|
|
|
|
0x0B,
|
|
|
|
|
0x8B,
|
|
|
|
|
0x4B,
|
|
|
|
|
0xCB,
|
|
|
|
|
0x2B,
|
|
|
|
|
0xAB,
|
|
|
|
|
0x6B,
|
|
|
|
|
0xEB,
|
|
|
|
|
0x1B,
|
|
|
|
|
0x9B,
|
|
|
|
|
0x5B,
|
|
|
|
|
0xDB,
|
|
|
|
|
0x3B,
|
|
|
|
|
0xBB,
|
|
|
|
|
0x7B,
|
|
|
|
|
0xFB,
|
|
|
|
|
0x07,
|
|
|
|
|
0x87,
|
|
|
|
|
0x47,
|
|
|
|
|
0xC7,
|
|
|
|
|
0x27,
|
|
|
|
|
0xA7,
|
|
|
|
|
0x67,
|
|
|
|
|
0xE7,
|
|
|
|
|
0x17,
|
|
|
|
|
0x97,
|
|
|
|
|
0x57,
|
|
|
|
|
0xD7,
|
|
|
|
|
0x37,
|
|
|
|
|
0xB7,
|
|
|
|
|
0x77,
|
|
|
|
|
0xF7,
|
|
|
|
|
0x0F,
|
|
|
|
|
0x8F,
|
|
|
|
|
0x4F,
|
|
|
|
|
0xCF,
|
|
|
|
|
0x2F,
|
|
|
|
|
0xAF,
|
|
|
|
|
0x6F,
|
|
|
|
|
0xEF,
|
|
|
|
|
0x1F,
|
|
|
|
|
0x9F,
|
|
|
|
|
0x5F,
|
|
|
|
|
0xDF,
|
|
|
|
|
0x3F,
|
|
|
|
|
0xBF,
|
|
|
|
|
0x7F,
|
|
|
|
|
0xFF,
|
|
|
|
|
]
|
|
|
|
|
|
2015-05-10 12:35:09 +00:00
|
|
|
|
|
2016-02-13 08:31:33 +00:00
|
|
|
|
class NegativeDimensionError(Exception):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class UnsupportedColorspaceError(Exception):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ImageOpenError(Exception):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
2016-02-16 23:37:13 +00:00
|
|
|
|
class JpegColorspaceError(Exception):
|
2016-02-13 08:31:33 +00:00
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class PdfTooLargeError(Exception):
|
2015-05-10 12:35:09 +00:00
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
2019-03-28 09:26:46 +00:00
|
|
|
|
class AlphaChannelError(Exception):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ExifOrientationError(Exception):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
2015-05-10 12:35:09 +00:00
|
|
|
|
# without pdfrw this function is a no-op
|
|
|
|
|
def my_convert_load(string):
|
|
|
|
|
return string
|
2014-03-01 04:51:53 +00:00
|
|
|
|
|
2015-03-13 13:29:53 +00:00
|
|
|
|
|
2012-03-29 09:08:32 +00:00
|
|
|
|
def parse(cont, indent=1):
|
|
|
|
|
if type(cont) is dict:
|
2018-12-25 11:30:10 +00:00
|
|
|
|
return (
|
|
|
|
|
b"<<\n"
|
|
|
|
|
+ b"\n".join(
|
|
|
|
|
[
|
|
|
|
|
4 * indent * b" " + k + b" " + parse(v, indent + 1)
|
|
|
|
|
for k, v in sorted(cont.items())
|
|
|
|
|
]
|
|
|
|
|
)
|
|
|
|
|
+ b"\n"
|
|
|
|
|
+ 4 * (indent - 1) * b" "
|
|
|
|
|
+ b">>"
|
|
|
|
|
)
|
2015-02-16 18:09:34 +00:00
|
|
|
|
elif type(cont) is int:
|
2015-03-13 10:43:38 +00:00
|
|
|
|
return str(cont).encode()
|
2015-02-16 18:09:34 +00:00
|
|
|
|
elif type(cont) is float:
|
2015-05-10 12:35:09 +00:00
|
|
|
|
if int(cont) == cont:
|
|
|
|
|
return parse(int(cont))
|
|
|
|
|
else:
|
|
|
|
|
return ("%0.4f" % cont).rstrip("0").encode()
|
|
|
|
|
elif isinstance(cont, MyPdfDict):
|
2016-06-10 14:39:45 +00:00
|
|
|
|
# if cont got an identifier, then addobj() has been called with it
|
2015-05-10 12:35:09 +00:00
|
|
|
|
# and a link to it will be added, otherwise add it inline
|
|
|
|
|
if hasattr(cont, "identifier"):
|
|
|
|
|
return ("%d 0 R" % cont.identifier).encode()
|
|
|
|
|
else:
|
|
|
|
|
return parse(cont.content, indent)
|
|
|
|
|
elif type(cont) is str or isinstance(cont, bytes):
|
2015-03-13 10:43:38 +00:00
|
|
|
|
if type(cont) is str and type(cont) is not bytes:
|
2016-02-13 08:31:33 +00:00
|
|
|
|
raise TypeError(
|
2018-12-25 11:30:10 +00:00
|
|
|
|
"parse must be passed a bytes object in py3. Got: %s" % cont
|
|
|
|
|
)
|
2012-03-29 09:08:32 +00:00
|
|
|
|
return cont
|
2015-05-10 12:35:09 +00:00
|
|
|
|
elif isinstance(cont, list):
|
2018-12-25 11:30:10 +00:00
|
|
|
|
return b"[ " + b" ".join([parse(c, indent) for c in cont]) + b" ]"
|
2015-03-13 10:43:38 +00:00
|
|
|
|
else:
|
2018-12-25 11:30:10 +00:00
|
|
|
|
raise TypeError("cannot handle type %s with content %s" % (type(cont), cont))
|
2015-05-10 12:35:09 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class MyPdfDict(object):
|
|
|
|
|
def __init__(self, *args, **kw):
|
|
|
|
|
self.content = dict()
|
|
|
|
|
if args:
|
|
|
|
|
if len(args) == 1:
|
|
|
|
|
args = args[0]
|
|
|
|
|
self.content.update(args)
|
|
|
|
|
self.stream = None
|
|
|
|
|
for key, value in kw.items():
|
|
|
|
|
if key == "stream":
|
|
|
|
|
self.stream = value
|
|
|
|
|
self.content[MyPdfName.Length] = len(value)
|
|
|
|
|
elif key == "indirect":
|
|
|
|
|
pass
|
|
|
|
|
else:
|
|
|
|
|
self.content[getattr(MyPdfName, key)] = value
|
2012-03-29 09:08:32 +00:00
|
|
|
|
|
2013-10-23 10:34:07 +00:00
|
|
|
|
def tostring(self):
|
2015-05-10 12:35:09 +00:00
|
|
|
|
if self.stream is not None:
|
2014-03-01 04:51:53 +00:00
|
|
|
|
return (
|
2018-12-25 11:30:10 +00:00
|
|
|
|
("%d 0 obj\n" % self.identifier).encode()
|
|
|
|
|
+ parse(self.content)
|
|
|
|
|
+ b"\nstream\n"
|
|
|
|
|
+ self.stream
|
|
|
|
|
+ b"\nendstream\nendobj\n"
|
|
|
|
|
)
|
2012-03-29 09:08:32 +00:00
|
|
|
|
else:
|
2018-12-25 11:30:10 +00:00
|
|
|
|
return (
|
|
|
|
|
("%d 0 obj\n" % self.identifier).encode()
|
|
|
|
|
+ parse(self.content)
|
|
|
|
|
+ b"\nendobj\n"
|
|
|
|
|
)
|
2012-03-29 09:08:32 +00:00
|
|
|
|
|
2015-05-10 12:35:09 +00:00
|
|
|
|
def __setitem__(self, key, value):
|
|
|
|
|
self.content[key] = value
|
2012-06-15 14:59:31 +00:00
|
|
|
|
|
2015-05-10 12:35:09 +00:00
|
|
|
|
def __getitem__(self, key):
|
|
|
|
|
return self.content[key]
|
|
|
|
|
|
2019-04-02 05:37:03 +00:00
|
|
|
|
def __contains__(self, key):
|
|
|
|
|
return key in self.content
|
|
|
|
|
|
2015-05-10 12:35:09 +00:00
|
|
|
|
|
2018-12-25 11:30:10 +00:00
|
|
|
|
class MyPdfName:
|
2015-05-10 12:35:09 +00:00
|
|
|
|
def __getattr__(self, name):
|
2018-12-25 11:30:10 +00:00
|
|
|
|
return b"/" + name.encode("ascii")
|
2017-01-19 10:22:08 +00:00
|
|
|
|
|
|
|
|
|
|
2015-05-10 12:35:09 +00:00
|
|
|
|
MyPdfName = MyPdfName()
|
2013-10-23 10:34:07 +00:00
|
|
|
|
|
|
|
|
|
|
2015-05-10 12:35:09 +00:00
|
|
|
|
class MyPdfObject(bytes):
|
|
|
|
|
def __new__(cls, string):
|
2018-12-25 11:30:10 +00:00
|
|
|
|
return bytes.__new__(cls, string.encode("ascii"))
|
2013-10-23 10:34:07 +00:00
|
|
|
|
|
2015-05-10 12:35:09 +00:00
|
|
|
|
|
|
|
|
|
class MyPdfArray(list):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
2018-12-25 11:30:10 +00:00
|
|
|
|
class MyPdfWriter:
|
2020-05-31 15:18:02 +00:00
|
|
|
|
def __init__(self):
|
2015-05-10 12:35:09 +00:00
|
|
|
|
self.objects = []
|
2014-03-01 03:57:40 +00:00
|
|
|
|
# create an incomplete pages object so that a /Parent entry can be
|
|
|
|
|
# added to each page
|
2015-05-10 12:35:09 +00:00
|
|
|
|
self.pages = MyPdfDict(Type=MyPdfName.Pages, Kids=[], Count=0)
|
|
|
|
|
self.catalog = MyPdfDict(Pages=self.pages, Type=MyPdfName.Catalog)
|
|
|
|
|
self.pagearray = []
|
2013-08-30 08:45:43 +00:00
|
|
|
|
|
2013-10-23 10:34:07 +00:00
|
|
|
|
def addobj(self, obj):
|
2018-12-25 11:30:10 +00:00
|
|
|
|
newid = len(self.objects) + 1
|
2013-10-23 10:34:07 +00:00
|
|
|
|
obj.identifier = newid
|
|
|
|
|
self.objects.append(obj)
|
|
|
|
|
|
2020-06-01 00:10:07 +00:00
|
|
|
|
def tostream(self, info, stream, version="1.3", ident=None):
|
2013-10-23 10:34:07 +00:00
|
|
|
|
xreftable = list()
|
|
|
|
|
|
2015-05-10 12:35:09 +00:00
|
|
|
|
# justification of the random binary garbage in the header from
|
|
|
|
|
# adobe:
|
|
|
|
|
#
|
|
|
|
|
# > Note: If a PDF file contains binary data, as most do (see Section
|
|
|
|
|
# > 3.1, “Lexical Conventions”), it is recommended that the header
|
|
|
|
|
# > line be immediately followed by a comment line containing at
|
|
|
|
|
# > least four binary characters—that is, characters whose codes are
|
|
|
|
|
# > 128 or greater. This ensures proper behavior of file transfer
|
|
|
|
|
# > applications that inspect data near the beginning of a file to
|
|
|
|
|
# > determine whether to treat the file’s contents as text or as
|
|
|
|
|
# > binary.
|
|
|
|
|
#
|
|
|
|
|
# the choice of binary characters is arbitrary but those four seem to
|
|
|
|
|
# be used elsewhere.
|
2020-05-31 15:18:02 +00:00
|
|
|
|
pdfheader = ("%%PDF-%s\n" % version).encode("ascii")
|
2018-12-25 11:30:10 +00:00
|
|
|
|
pdfheader += b"%\xe2\xe3\xcf\xd3\n"
|
2016-02-13 07:51:20 +00:00
|
|
|
|
stream.write(pdfheader)
|
2015-05-10 12:35:09 +00:00
|
|
|
|
|
|
|
|
|
# From section 3.4.3 of the PDF Reference (version 1.7):
|
|
|
|
|
#
|
|
|
|
|
# > Each entry is exactly 20 bytes long, including the end-of-line
|
|
|
|
|
# > marker.
|
|
|
|
|
# >
|
|
|
|
|
# > [...]
|
|
|
|
|
# >
|
|
|
|
|
# > The format of an in-use entry is
|
|
|
|
|
# > nnnnnnnnnn ggggg n eol
|
|
|
|
|
# > where
|
|
|
|
|
# > nnnnnnnnnn is a 10-digit byte offset
|
|
|
|
|
# > ggggg is a 5-digit generation number
|
|
|
|
|
# > n is a literal keyword identifying this as an in-use entry
|
|
|
|
|
# > eol is a 2-character end-of-line sequence
|
|
|
|
|
# >
|
|
|
|
|
# > [...]
|
|
|
|
|
# >
|
|
|
|
|
# > If the file’s end-of-line marker is a single character (either a
|
|
|
|
|
# > carriage return or a line feed), it is preceded by a single space;
|
|
|
|
|
#
|
2016-06-10 14:39:45 +00:00
|
|
|
|
# Since we chose to use a single character eol marker, we precede it by
|
2015-05-10 12:35:09 +00:00
|
|
|
|
# a space
|
2016-02-13 07:51:20 +00:00
|
|
|
|
pos = len(pdfheader)
|
2015-01-07 14:56:24 +00:00
|
|
|
|
xreftable.append(b"0000000000 65535 f \n")
|
2013-10-23 10:34:07 +00:00
|
|
|
|
for o in self.objects:
|
2016-02-13 07:51:20 +00:00
|
|
|
|
xreftable.append(("%010d 00000 n \n" % pos).encode())
|
|
|
|
|
content = o.tostring()
|
|
|
|
|
stream.write(content)
|
|
|
|
|
pos += len(content)
|
|
|
|
|
|
|
|
|
|
xrefoffset = pos
|
|
|
|
|
stream.write(b"xref\n")
|
|
|
|
|
stream.write(("0 %d\n" % len(xreftable)).encode())
|
2013-10-23 10:34:07 +00:00
|
|
|
|
for x in xreftable:
|
2016-02-13 07:51:20 +00:00
|
|
|
|
stream.write(x)
|
|
|
|
|
stream.write(b"trailer\n")
|
2020-06-01 00:10:07 +00:00
|
|
|
|
trailer = {b"/Size": len(xreftable), b"/Info": info, b"/Root": self.catalog}
|
|
|
|
|
if ident is not None:
|
|
|
|
|
md5 = hashlib.md5(ident).hexdigest().encode("ascii")
|
|
|
|
|
trailer[b"/ID"] = b"[<%s><%s>]" % (md5, md5)
|
|
|
|
|
stream.write(parse(trailer) + b"\n")
|
2016-02-13 07:51:20 +00:00
|
|
|
|
stream.write(b"startxref\n")
|
|
|
|
|
stream.write(("%d\n" % xrefoffset).encode())
|
|
|
|
|
stream.write(b"%%EOF\n")
|
|
|
|
|
return
|
2012-03-29 09:08:32 +00:00
|
|
|
|
|
2015-05-10 12:35:09 +00:00
|
|
|
|
def addpage(self, page):
|
|
|
|
|
page[b"/Parent"] = self.pages
|
|
|
|
|
self.pagearray.append(page)
|
|
|
|
|
self.pages.content[b"/Kids"].append(page)
|
|
|
|
|
self.pages.content[b"/Count"] += 1
|
|
|
|
|
self.addobj(page)
|
|
|
|
|
|
|
|
|
|
|
2020-04-23 05:13:39 +00:00
|
|
|
|
class MyPdfString:
|
|
|
|
|
@classmethod
|
|
|
|
|
def encode(cls, string, hextype=False):
|
|
|
|
|
if hextype:
|
|
|
|
|
return (
|
|
|
|
|
b"< " + b" ".join(("%06x" % c).encode("ascii") for c in string) + b" >"
|
|
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
try:
|
|
|
|
|
string = string.encode("ascii")
|
|
|
|
|
except UnicodeEncodeError:
|
|
|
|
|
string = b"\xfe\xff" + string.encode("utf-16-be")
|
|
|
|
|
# We should probably encode more here because at least
|
|
|
|
|
# ghostscript interpretes a carriage return byte (0x0D) as a
|
|
|
|
|
# new line byte (0x0A)
|
|
|
|
|
# PDF supports: \n, \r, \t, \b and \f
|
|
|
|
|
string = string.replace(b"\\", b"\\\\")
|
|
|
|
|
string = string.replace(b"(", b"\\(")
|
|
|
|
|
string = string.replace(b")", b"\\)")
|
|
|
|
|
return b"(" + string + b")"
|
2015-05-10 12:35:09 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class pdfdoc(object):
|
2018-12-25 11:30:10 +00:00
|
|
|
|
def __init__(
|
|
|
|
|
self,
|
2020-05-28 10:46:37 +00:00
|
|
|
|
engine=Engine.internal,
|
2018-12-25 11:30:10 +00:00
|
|
|
|
version="1.3",
|
|
|
|
|
title=None,
|
|
|
|
|
author=None,
|
|
|
|
|
creator=None,
|
|
|
|
|
producer=None,
|
|
|
|
|
creationdate=None,
|
|
|
|
|
moddate=None,
|
|
|
|
|
subject=None,
|
|
|
|
|
keywords=None,
|
|
|
|
|
nodate=False,
|
|
|
|
|
panes=None,
|
|
|
|
|
initial_page=None,
|
|
|
|
|
magnification=None,
|
|
|
|
|
page_layout=None,
|
|
|
|
|
fit_window=False,
|
|
|
|
|
center_window=False,
|
|
|
|
|
fullscreen=False,
|
2020-06-01 00:10:07 +00:00
|
|
|
|
pdfa=None,
|
2018-12-25 11:30:10 +00:00
|
|
|
|
):
|
2020-05-28 10:46:37 +00:00
|
|
|
|
if engine is None:
|
2020-05-31 15:18:02 +00:00
|
|
|
|
if have_pikepdf:
|
2020-06-05 23:19:13 +00:00
|
|
|
|
engine = Engine.pikepdf
|
2020-05-31 15:18:02 +00:00
|
|
|
|
elif have_pdfrw:
|
2020-05-28 10:46:37 +00:00
|
|
|
|
engine = Engine.pdfrw
|
|
|
|
|
else:
|
|
|
|
|
engine = Engine.internal
|
|
|
|
|
|
2020-05-31 15:18:02 +00:00
|
|
|
|
if engine == Engine.pikepdf:
|
|
|
|
|
PdfWriter = pikepdf.new
|
|
|
|
|
PdfDict = pikepdf.Dictionary
|
|
|
|
|
PdfName = pikepdf.Name
|
|
|
|
|
elif engine == Engine.pdfrw:
|
2020-04-23 04:59:07 +00:00
|
|
|
|
from pdfrw import PdfWriter, PdfDict, PdfName, PdfString
|
2020-05-28 10:46:37 +00:00
|
|
|
|
elif engine == Engine.internal:
|
2015-05-10 12:35:09 +00:00
|
|
|
|
PdfWriter = MyPdfWriter
|
|
|
|
|
PdfDict = MyPdfDict
|
|
|
|
|
PdfName = MyPdfName
|
|
|
|
|
PdfString = MyPdfString
|
2020-05-28 10:46:37 +00:00
|
|
|
|
else:
|
|
|
|
|
raise ValueError("unknown engine: %s" % engine)
|
2015-05-10 12:35:09 +00:00
|
|
|
|
|
2020-05-31 15:18:02 +00:00
|
|
|
|
self.writer = PdfWriter()
|
|
|
|
|
if engine != Engine.pikepdf:
|
|
|
|
|
self.writer.docinfo = PdfDict(indirect=True)
|
2015-05-10 12:35:09 +00:00
|
|
|
|
|
|
|
|
|
def datetime_to_pdfdate(dt):
|
|
|
|
|
return dt.strftime("%Y%m%d%H%M%SZ")
|
|
|
|
|
|
2020-05-31 15:18:02 +00:00
|
|
|
|
for k in ["Title", "Author", "Creator", "Producer", "Subject"]:
|
|
|
|
|
v = locals()[k.lower()]
|
|
|
|
|
if v is None or v == "":
|
|
|
|
|
continue
|
|
|
|
|
if engine != Engine.pikepdf:
|
|
|
|
|
v = PdfString.encode(v)
|
2020-06-01 00:10:07 +00:00
|
|
|
|
self.writer.docinfo[getattr(PdfName, k)] = v
|
2020-05-31 15:18:02 +00:00
|
|
|
|
|
|
|
|
|
now = datetime.now()
|
|
|
|
|
for k in ["CreationDate", "ModDate"]:
|
|
|
|
|
v = locals()[k.lower()]
|
|
|
|
|
if v is None and nodate:
|
|
|
|
|
continue
|
|
|
|
|
if v is None:
|
|
|
|
|
v = now
|
|
|
|
|
v = ("D:" + datetime_to_pdfdate(v)).encode("ascii")
|
2020-06-01 00:10:07 +00:00
|
|
|
|
if engine == Engine.internal:
|
|
|
|
|
v = b"(" + v + b")"
|
2020-05-31 15:18:02 +00:00
|
|
|
|
self.writer.docinfo[getattr(PdfName, k)] = v
|
2015-05-10 12:35:09 +00:00
|
|
|
|
if keywords is not None:
|
2020-05-31 15:18:02 +00:00
|
|
|
|
if engine == Engine.pikepdf:
|
|
|
|
|
self.writer.docinfo[PdfName.Keywords] = ",".join(keywords)
|
|
|
|
|
else:
|
|
|
|
|
self.writer.docinfo[PdfName.Keywords] = PdfString.encode(
|
|
|
|
|
",".join(keywords)
|
|
|
|
|
)
|
2015-05-10 12:35:09 +00:00
|
|
|
|
|
2020-06-01 00:10:07 +00:00
|
|
|
|
def datetime_to_xmpdate(dt):
|
|
|
|
|
return dt.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
|
|
|
|
|
|
|
|
self.xmp = b"""<?xpacket begin='\xef\xbb\xbf' id='W5M0MpCehiHzreSzNTczkc9d'?>
|
|
|
|
|
<x:xmpmeta xmlns:x='adobe:ns:meta/' x:xmptk='XMP toolkit 2.9.1-13, framework 1.6'>
|
|
|
|
|
<rdf:RDF xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#' xmlns:iX='http://ns.adobe.com/iX/1.0/'>
|
|
|
|
|
<rdf:Description rdf:about='' xmlns:pdf='http://ns.adobe.com/pdf/1.3/'%s/>
|
|
|
|
|
<rdf:Description rdf:about='' xmlns:xmp='http://ns.adobe.com/xap/1.0/'>
|
|
|
|
|
%s
|
|
|
|
|
%s
|
|
|
|
|
</rdf:Description>
|
|
|
|
|
<rdf:Description rdf:about='' xmlns:pdfaid='http://www.aiim.org/pdfa/ns/id/' pdfaid:part='1' pdfaid:conformance='B'/>
|
|
|
|
|
</rdf:RDF>
|
|
|
|
|
</x:xmpmeta>
|
|
|
|
|
|
|
|
|
|
<?xpacket end='w'?>
|
|
|
|
|
""" % (
|
|
|
|
|
b" pdf:Producer='%s'" % producer.encode("ascii")
|
|
|
|
|
if producer is not None
|
|
|
|
|
else b"",
|
|
|
|
|
b""
|
|
|
|
|
if creationdate is None and nodate
|
|
|
|
|
else b"<xmp:ModifyDate>%s</xmp:ModifyDate>"
|
|
|
|
|
% datetime_to_xmpdate(now if creationdate is None else creationdate).encode(
|
|
|
|
|
"ascii"
|
|
|
|
|
),
|
|
|
|
|
b""
|
|
|
|
|
if moddate is None and nodate
|
|
|
|
|
else b"<xmp:CreateDate>%s</xmp:CreateDate>"
|
|
|
|
|
% datetime_to_xmpdate(now if moddate is None else moddate).encode("ascii"),
|
|
|
|
|
)
|
|
|
|
|
|
2020-05-31 15:18:02 +00:00
|
|
|
|
if engine != Engine.pikepdf:
|
|
|
|
|
# this is done because pdfrw adds info, catalog and pages as the first
|
|
|
|
|
# three objects in this order
|
|
|
|
|
if engine == Engine.internal:
|
|
|
|
|
self.writer.addobj(self.writer.docinfo)
|
|
|
|
|
self.writer.addobj(self.writer.catalog)
|
|
|
|
|
self.writer.addobj(self.writer.pages)
|
2015-05-10 12:35:09 +00:00
|
|
|
|
|
|
|
|
|
self.panes = panes
|
|
|
|
|
self.initial_page = initial_page
|
|
|
|
|
self.magnification = magnification
|
|
|
|
|
self.page_layout = page_layout
|
|
|
|
|
self.fit_window = fit_window
|
|
|
|
|
self.center_window = center_window
|
|
|
|
|
self.fullscreen = fullscreen
|
2020-05-28 10:46:37 +00:00
|
|
|
|
self.engine = engine
|
2020-05-31 15:18:02 +00:00
|
|
|
|
self.output_version = version
|
2020-06-01 00:10:07 +00:00
|
|
|
|
self.pdfa = pdfa
|
2015-05-10 12:35:09 +00:00
|
|
|
|
|
2018-12-25 11:30:10 +00:00
|
|
|
|
def add_imagepage(
|
|
|
|
|
self,
|
|
|
|
|
color,
|
|
|
|
|
imgwidthpx,
|
|
|
|
|
imgheightpx,
|
|
|
|
|
imgformat,
|
|
|
|
|
imgdata,
|
2021-06-12 20:03:56 +00:00
|
|
|
|
smaskdata,
|
2018-12-25 11:30:10 +00:00
|
|
|
|
imgwidthpdf,
|
|
|
|
|
imgheightpdf,
|
|
|
|
|
imgxpdf,
|
|
|
|
|
imgypdf,
|
|
|
|
|
pagewidth,
|
|
|
|
|
pageheight,
|
|
|
|
|
userunit=None,
|
|
|
|
|
palette=None,
|
|
|
|
|
inverted=False,
|
|
|
|
|
depth=0,
|
|
|
|
|
rotate=0,
|
2019-04-02 05:37:03 +00:00
|
|
|
|
cropborder=None,
|
|
|
|
|
bleedborder=None,
|
|
|
|
|
trimborder=None,
|
|
|
|
|
artborder=None,
|
2020-08-06 22:13:53 +00:00
|
|
|
|
iccp=None,
|
2018-12-25 11:30:10 +00:00
|
|
|
|
):
|
2021-06-12 20:03:56 +00:00
|
|
|
|
assert color != Colorspace.RGBA or (imgformat == ImageFormat.PNG and smaskdata is not None)
|
|
|
|
|
|
2020-05-31 15:18:02 +00:00
|
|
|
|
if self.engine == Engine.pikepdf:
|
|
|
|
|
PdfArray = pikepdf.Array
|
|
|
|
|
PdfDict = pikepdf.Dictionary
|
|
|
|
|
PdfName = pikepdf.Name
|
|
|
|
|
elif self.engine == Engine.pdfrw:
|
2018-03-15 10:31:36 +00:00
|
|
|
|
from pdfrw import PdfDict, PdfName, PdfObject, PdfString
|
2015-05-10 12:35:09 +00:00
|
|
|
|
from pdfrw.py23_diffs import convert_load
|
2020-05-28 10:46:37 +00:00
|
|
|
|
elif self.engine == Engine.internal:
|
2015-05-10 12:35:09 +00:00
|
|
|
|
PdfDict = MyPdfDict
|
|
|
|
|
PdfName = MyPdfName
|
2016-07-21 10:49:56 +00:00
|
|
|
|
PdfObject = MyPdfObject
|
2018-03-15 10:31:36 +00:00
|
|
|
|
PdfString = MyPdfString
|
2015-05-10 12:35:09 +00:00
|
|
|
|
convert_load = my_convert_load
|
2020-05-28 10:46:37 +00:00
|
|
|
|
else:
|
|
|
|
|
raise ValueError("unknown engine: %s" % self.engine)
|
2020-05-31 15:18:02 +00:00
|
|
|
|
TrueObject = True if self.engine == Engine.pikepdf else PdfObject("true")
|
|
|
|
|
FalseObject = False if self.engine == Engine.pikepdf else PdfObject("false")
|
2015-05-10 12:35:09 +00:00
|
|
|
|
|
2018-12-25 11:30:10 +00:00
|
|
|
|
if color == Colorspace["1"] or color == Colorspace.L:
|
2015-05-10 12:35:09 +00:00
|
|
|
|
colorspace = PdfName.DeviceGray
|
2021-06-12 20:03:56 +00:00
|
|
|
|
elif color == Colorspace.RGB or color == Colorspace.RGBA:
|
2015-05-10 12:35:09 +00:00
|
|
|
|
colorspace = PdfName.DeviceRGB
|
2018-12-25 11:30:10 +00:00
|
|
|
|
elif color == Colorspace.CMYK or color == Colorspace["CMYK;I"]:
|
2015-05-10 12:35:09 +00:00
|
|
|
|
colorspace = PdfName.DeviceCMYK
|
2018-03-15 10:31:36 +00:00
|
|
|
|
elif color == Colorspace.P:
|
2020-05-28 10:46:37 +00:00
|
|
|
|
if self.engine == Engine.pdfrw:
|
2020-05-31 15:18:02 +00:00
|
|
|
|
# https://github.com/pmaupin/pdfrw/issues/128
|
|
|
|
|
# https://github.com/pmaupin/pdfrw/issues/147
|
2018-12-25 11:30:10 +00:00
|
|
|
|
raise Exception(
|
|
|
|
|
"pdfrw does not support hex strings for "
|
|
|
|
|
"palette image input, re-run with "
|
2020-05-31 15:18:02 +00:00
|
|
|
|
"--engine=internal or --engine=pikepdf"
|
2018-12-25 11:30:10 +00:00
|
|
|
|
)
|
2020-05-31 15:18:02 +00:00
|
|
|
|
assert len(palette) % 3 == 0
|
2018-12-25 11:30:10 +00:00
|
|
|
|
colorspace = [
|
|
|
|
|
PdfName.Indexed,
|
|
|
|
|
PdfName.DeviceRGB,
|
2020-05-31 15:18:02 +00:00
|
|
|
|
(len(palette) // 3) - 1,
|
|
|
|
|
bytes(palette)
|
|
|
|
|
if self.engine == Engine.pikepdf
|
|
|
|
|
else PdfString.encode(
|
|
|
|
|
[
|
|
|
|
|
int.from_bytes(palette[i : i + 3], "big")
|
|
|
|
|
for i in range(0, len(palette), 3)
|
|
|
|
|
],
|
|
|
|
|
hextype=True,
|
|
|
|
|
),
|
2018-12-25 11:30:10 +00:00
|
|
|
|
]
|
2015-05-10 12:35:09 +00:00
|
|
|
|
else:
|
2018-12-25 11:30:10 +00:00
|
|
|
|
raise UnsupportedColorspaceError("unsupported color space: %s" % color.name)
|
2015-05-10 12:35:09 +00:00
|
|
|
|
|
2020-08-06 22:13:53 +00:00
|
|
|
|
if iccp is not None:
|
|
|
|
|
if self.engine == Engine.pikepdf:
|
|
|
|
|
iccpdict = self.writer.make_stream(iccp)
|
|
|
|
|
else:
|
|
|
|
|
iccpdict = PdfDict(stream=convert_load(iccp))
|
|
|
|
|
iccpdict[PdfName.Alternate] = colorspace
|
|
|
|
|
if color == Colorspace["1"] or color == Colorspace.L:
|
|
|
|
|
iccpdict[PdfName.N] = 1
|
2021-06-12 20:03:56 +00:00
|
|
|
|
elif color == Colorspace.RGB or color == Colorspace.RGBA:
|
2020-08-06 22:13:53 +00:00
|
|
|
|
iccpdict[PdfName.N] = 3
|
|
|
|
|
elif color == Colorspace.CMYK or color == Colorspace["CMYK;I"]:
|
|
|
|
|
iccpdict[PdfName.N] = 4
|
|
|
|
|
elif color == Colorspace.P:
|
|
|
|
|
raise Exception("Cannot have Palette images with ICC profile")
|
|
|
|
|
colorspace = [PdfName.ICCBased, iccpdict]
|
|
|
|
|
|
2015-05-10 12:35:09 +00:00
|
|
|
|
# either embed the whole jpeg or deflate the bitmap representation
|
|
|
|
|
if imgformat is ImageFormat.JPEG:
|
2018-07-17 21:59:55 +00:00
|
|
|
|
ofilter = PdfName.DCTDecode
|
2015-05-10 12:35:09 +00:00
|
|
|
|
elif imgformat is ImageFormat.JPEG2000:
|
2018-07-17 21:59:55 +00:00
|
|
|
|
ofilter = PdfName.JPXDecode
|
2020-05-31 15:18:02 +00:00
|
|
|
|
self.output_version = "1.5" # jpeg2000 needs pdf 1.5
|
2016-07-21 10:49:56 +00:00
|
|
|
|
elif imgformat is ImageFormat.CCITTGroup4:
|
2018-07-17 23:14:06 +00:00
|
|
|
|
ofilter = [PdfName.CCITTFaxDecode]
|
2015-05-10 12:35:09 +00:00
|
|
|
|
else:
|
2018-03-15 10:31:36 +00:00
|
|
|
|
ofilter = PdfName.FlateDecode
|
2015-05-10 12:35:09 +00:00
|
|
|
|
|
2020-05-31 15:18:02 +00:00
|
|
|
|
if self.engine == Engine.pikepdf:
|
|
|
|
|
image = self.writer.make_stream(imgdata)
|
|
|
|
|
else:
|
|
|
|
|
image = PdfDict(stream=convert_load(imgdata))
|
2015-05-10 12:35:09 +00:00
|
|
|
|
|
|
|
|
|
image[PdfName.Type] = PdfName.XObject
|
|
|
|
|
image[PdfName.Subtype] = PdfName.Image
|
|
|
|
|
image[PdfName.Filter] = ofilter
|
|
|
|
|
image[PdfName.Width] = imgwidthpx
|
|
|
|
|
image[PdfName.Height] = imgheightpx
|
|
|
|
|
image[PdfName.ColorSpace] = colorspace
|
2018-08-17 10:55:55 +00:00
|
|
|
|
image[PdfName.BitsPerComponent] = depth
|
2015-05-10 12:35:09 +00:00
|
|
|
|
|
2018-12-25 11:30:10 +00:00
|
|
|
|
if color == Colorspace["CMYK;I"]:
|
2015-05-10 12:35:09 +00:00
|
|
|
|
# Inverts all four channels
|
2020-05-31 15:18:02 +00:00
|
|
|
|
image[PdfName.Decode] = [1, 0, 1, 0, 1, 0, 1, 0]
|
2015-05-10 12:35:09 +00:00
|
|
|
|
|
2016-07-21 10:49:56 +00:00
|
|
|
|
if imgformat is ImageFormat.CCITTGroup4:
|
|
|
|
|
decodeparms = PdfDict()
|
2018-08-01 20:12:40 +00:00
|
|
|
|
# The default for the K parameter is 0 which indicates Group 3 1-D
|
|
|
|
|
# encoding. We set it to -1 because we want Group 4 encoding.
|
2016-07-21 10:49:56 +00:00
|
|
|
|
decodeparms[PdfName.K] = -1
|
2018-08-01 20:12:40 +00:00
|
|
|
|
if inverted:
|
2020-05-31 15:18:02 +00:00
|
|
|
|
decodeparms[PdfName.BlackIs1] = FalseObject
|
2018-08-01 20:12:40 +00:00
|
|
|
|
else:
|
2020-05-31 15:18:02 +00:00
|
|
|
|
decodeparms[PdfName.BlackIs1] = TrueObject
|
2016-07-21 10:49:56 +00:00
|
|
|
|
decodeparms[PdfName.Columns] = imgwidthpx
|
|
|
|
|
decodeparms[PdfName.Rows] = imgheightpx
|
|
|
|
|
image[PdfName.DecodeParms] = [decodeparms]
|
2018-03-15 10:31:36 +00:00
|
|
|
|
elif imgformat is ImageFormat.PNG:
|
2021-06-12 20:03:56 +00:00
|
|
|
|
if color == Colorspace.RGBA:
|
|
|
|
|
if self.engine == Engine.pikepdf:
|
|
|
|
|
smask = self.writer.make_stream(smaskdata)
|
|
|
|
|
else:
|
|
|
|
|
smask = PdfDict(stream=convert_load(smaskdata))
|
|
|
|
|
smask[PdfName.Type] = PdfName.XObject
|
|
|
|
|
smask[PdfName.Subtype] = PdfName.Image
|
|
|
|
|
smask[PdfName.Filter] = PdfName.FlateDecode
|
|
|
|
|
smask[PdfName.Width] = imgwidthpx
|
|
|
|
|
smask[PdfName.Height] = imgheightpx
|
|
|
|
|
smask[PdfName.ColorSpace] = PdfName.DeviceGray
|
|
|
|
|
smask[PdfName.BitsPerComponent] = depth
|
|
|
|
|
|
|
|
|
|
image[PdfName.SMask] = smask
|
|
|
|
|
|
|
|
|
|
# /SMask requires PDF 1.4
|
|
|
|
|
if self.output_version < "1.4":
|
|
|
|
|
self.output_version = "1.4"
|
2018-03-15 10:31:36 +00:00
|
|
|
|
else:
|
2021-06-12 20:03:56 +00:00
|
|
|
|
decodeparms = PdfDict()
|
|
|
|
|
decodeparms[PdfName.Predictor] = 15
|
|
|
|
|
if color in [Colorspace.P, Colorspace["1"], Colorspace.L]:
|
|
|
|
|
decodeparms[PdfName.Colors] = 1
|
|
|
|
|
else:
|
|
|
|
|
decodeparms[PdfName.Colors] = 3
|
|
|
|
|
decodeparms[PdfName.Columns] = imgwidthpx
|
|
|
|
|
decodeparms[PdfName.BitsPerComponent] = depth
|
|
|
|
|
image[PdfName.DecodeParms] = decodeparms
|
2016-07-21 10:49:56 +00:00
|
|
|
|
|
2018-12-25 11:30:10 +00:00
|
|
|
|
text = (
|
|
|
|
|
"q\n%0.4f 0 0 %0.4f %0.4f %0.4f cm\n/Im0 Do\nQ"
|
|
|
|
|
% (imgwidthpdf, imgheightpdf, imgxpdf, imgypdf)
|
|
|
|
|
).encode("ascii")
|
2015-03-20 10:37:30 +00:00
|
|
|
|
|
2020-05-31 15:18:02 +00:00
|
|
|
|
if self.engine == Engine.pikepdf:
|
|
|
|
|
content = self.writer.make_stream(text)
|
|
|
|
|
else:
|
|
|
|
|
content = PdfDict(stream=convert_load(text))
|
2015-05-10 12:35:09 +00:00
|
|
|
|
resources = PdfDict(XObject=PdfDict(Im0=image))
|
2012-03-29 09:08:32 +00:00
|
|
|
|
|
2020-05-31 15:18:02 +00:00
|
|
|
|
if self.engine == Engine.pikepdf:
|
|
|
|
|
page = self.writer.add_blank_page(page_size=(pagewidth, pageheight))
|
|
|
|
|
else:
|
|
|
|
|
page = PdfDict(indirect=True)
|
|
|
|
|
page[PdfName.Type] = PdfName.Page
|
|
|
|
|
page[PdfName.MediaBox] = [0, 0, pagewidth, pageheight]
|
2019-04-02 05:37:03 +00:00
|
|
|
|
# 14.11.2 Page Boundaries
|
|
|
|
|
# ...
|
|
|
|
|
# The crop, bleed, trim, and art boxes shall not ordinarily extend
|
|
|
|
|
# beyond the boundaries of the media box. If they do, they are
|
|
|
|
|
# effectively reduced to their intersection with the media box.
|
|
|
|
|
if cropborder is not None:
|
|
|
|
|
page[PdfName.CropBox] = [
|
|
|
|
|
cropborder[1],
|
|
|
|
|
cropborder[0],
|
2021-04-13 11:10:34 +00:00
|
|
|
|
pagewidth - cropborder[1],
|
|
|
|
|
pageheight - cropborder[0],
|
2019-04-02 05:37:03 +00:00
|
|
|
|
]
|
|
|
|
|
if bleedborder is None:
|
|
|
|
|
if PdfName.CropBox in page:
|
|
|
|
|
page[PdfName.BleedBox] = page[PdfName.CropBox]
|
|
|
|
|
else:
|
|
|
|
|
page[PdfName.BleedBox] = [
|
|
|
|
|
bleedborder[1],
|
|
|
|
|
bleedborder[0],
|
2021-04-13 11:10:34 +00:00
|
|
|
|
pagewidth - bleedborder[1],
|
|
|
|
|
pageheight - bleedborder[0],
|
2019-04-02 05:37:03 +00:00
|
|
|
|
]
|
|
|
|
|
if trimborder is None:
|
|
|
|
|
if PdfName.CropBox in page:
|
|
|
|
|
page[PdfName.TrimBox] = page[PdfName.CropBox]
|
|
|
|
|
else:
|
|
|
|
|
page[PdfName.TrimBox] = [
|
|
|
|
|
trimborder[1],
|
|
|
|
|
trimborder[0],
|
2021-04-13 11:10:34 +00:00
|
|
|
|
pagewidth - trimborder[1],
|
|
|
|
|
pageheight - trimborder[0],
|
2019-04-02 05:37:03 +00:00
|
|
|
|
]
|
|
|
|
|
if artborder is None:
|
|
|
|
|
if PdfName.CropBox in page:
|
|
|
|
|
page[PdfName.ArtBox] = page[PdfName.CropBox]
|
|
|
|
|
else:
|
|
|
|
|
page[PdfName.ArtBox] = [
|
|
|
|
|
artborder[1],
|
|
|
|
|
artborder[0],
|
2021-04-13 11:10:34 +00:00
|
|
|
|
pagewidth - artborder[1],
|
|
|
|
|
pageheight - artborder[0],
|
2019-04-02 05:37:03 +00:00
|
|
|
|
]
|
2015-05-10 12:35:09 +00:00
|
|
|
|
page[PdfName.Resources] = resources
|
|
|
|
|
page[PdfName.Contents] = content
|
2018-12-25 00:45:38 +00:00
|
|
|
|
if rotate != 0:
|
|
|
|
|
page[PdfName.Rotate] = rotate
|
2017-07-28 20:52:33 +00:00
|
|
|
|
if userunit is not None:
|
|
|
|
|
# /UserUnit requires PDF 1.6
|
2020-05-31 15:18:02 +00:00
|
|
|
|
if self.output_version < "1.6":
|
|
|
|
|
self.output_version = "1.6"
|
2017-07-28 20:52:33 +00:00
|
|
|
|
page[PdfName.UserUnit] = userunit
|
2012-03-29 09:08:32 +00:00
|
|
|
|
|
2020-05-31 15:18:02 +00:00
|
|
|
|
if self.engine != Engine.pikepdf:
|
|
|
|
|
self.writer.addpage(page)
|
2015-05-10 12:35:09 +00:00
|
|
|
|
|
2020-05-31 15:18:02 +00:00
|
|
|
|
if self.engine == Engine.internal:
|
|
|
|
|
self.writer.addobj(content)
|
|
|
|
|
self.writer.addobj(image)
|
2021-06-12 20:03:56 +00:00
|
|
|
|
if smask is not None:
|
|
|
|
|
self.writer.addobj(smask)
|
2020-08-06 22:13:53 +00:00
|
|
|
|
if iccp is not None:
|
|
|
|
|
self.writer.addobj(iccpdict)
|
2015-05-10 12:35:09 +00:00
|
|
|
|
|
|
|
|
|
def tostring(self):
|
2016-02-13 07:51:20 +00:00
|
|
|
|
stream = BytesIO()
|
|
|
|
|
self.tostream(stream)
|
|
|
|
|
return stream.getvalue()
|
|
|
|
|
|
|
|
|
|
def tostream(self, outputstream):
|
2020-05-31 15:18:02 +00:00
|
|
|
|
if self.engine == Engine.pikepdf:
|
|
|
|
|
PdfArray = pikepdf.Array
|
|
|
|
|
PdfDict = pikepdf.Dictionary
|
|
|
|
|
PdfName = pikepdf.Name
|
|
|
|
|
elif self.engine == Engine.pdfrw:
|
2015-05-10 12:35:09 +00:00
|
|
|
|
from pdfrw import PdfDict, PdfName, PdfArray, PdfObject
|
2020-06-01 00:10:07 +00:00
|
|
|
|
from pdfrw.py23_diffs import convert_load
|
2020-05-28 10:46:37 +00:00
|
|
|
|
elif self.engine == Engine.internal:
|
2015-05-10 12:35:09 +00:00
|
|
|
|
PdfDict = MyPdfDict
|
|
|
|
|
PdfName = MyPdfName
|
|
|
|
|
PdfObject = MyPdfObject
|
|
|
|
|
PdfArray = MyPdfArray
|
2020-06-01 00:10:07 +00:00
|
|
|
|
convert_load = my_convert_load
|
2020-05-28 10:46:37 +00:00
|
|
|
|
else:
|
|
|
|
|
raise ValueError("unknown engine: %s" % self.engine)
|
2020-05-31 15:18:02 +00:00
|
|
|
|
NullObject = None if self.engine == Engine.pikepdf else PdfObject("null")
|
|
|
|
|
TrueObject = True if self.engine == Engine.pikepdf else PdfObject("true")
|
2015-05-10 12:35:09 +00:00
|
|
|
|
|
|
|
|
|
# We fill the catalog with more information like /ViewerPreferences,
|
|
|
|
|
# /PageMode, /PageLayout or /OpenAction because the latter refers to a
|
|
|
|
|
# page object which has to be present so that we can get its id.
|
|
|
|
|
#
|
|
|
|
|
# Furthermore, if using pdfrw, the trailer is cleared every time a page
|
|
|
|
|
# is added, so we can only start using it after all pages have been
|
|
|
|
|
# written.
|
|
|
|
|
|
2020-05-31 15:18:02 +00:00
|
|
|
|
if self.engine == Engine.pikepdf:
|
|
|
|
|
catalog = self.writer.Root
|
|
|
|
|
elif self.engine == Engine.pdfrw:
|
2015-05-10 12:35:09 +00:00
|
|
|
|
catalog = self.writer.trailer.Root
|
2020-05-28 10:46:37 +00:00
|
|
|
|
elif self.engine == Engine.internal:
|
2015-05-10 12:35:09 +00:00
|
|
|
|
catalog = self.writer.catalog
|
2020-05-28 10:46:37 +00:00
|
|
|
|
else:
|
|
|
|
|
raise ValueError("unknown engine: %s" % self.engine)
|
2015-05-10 12:35:09 +00:00
|
|
|
|
|
2018-12-25 11:30:10 +00:00
|
|
|
|
if (
|
|
|
|
|
self.fullscreen
|
|
|
|
|
or self.fit_window
|
|
|
|
|
or self.center_window
|
|
|
|
|
or self.panes is not None
|
|
|
|
|
):
|
2015-05-10 12:35:09 +00:00
|
|
|
|
catalog[PdfName.ViewerPreferences] = PdfDict()
|
|
|
|
|
|
|
|
|
|
if self.fullscreen:
|
|
|
|
|
# this setting might be overwritten later by the page mode
|
2018-12-25 11:30:10 +00:00
|
|
|
|
catalog[PdfName.ViewerPreferences][
|
|
|
|
|
PdfName.NonFullScreenPageMode
|
|
|
|
|
] = PdfName.UseNone
|
2015-05-10 12:35:09 +00:00
|
|
|
|
|
|
|
|
|
if self.panes == PageMode.thumbs:
|
2018-12-25 11:30:10 +00:00
|
|
|
|
catalog[PdfName.ViewerPreferences][
|
|
|
|
|
PdfName.NonFullScreenPageMode
|
|
|
|
|
] = PdfName.UseThumbs
|
2015-05-10 12:35:09 +00:00
|
|
|
|
# this setting might be overwritten later if fullscreen
|
|
|
|
|
catalog[PdfName.PageMode] = PdfName.UseThumbs
|
|
|
|
|
elif self.panes == PageMode.outlines:
|
2018-12-25 11:30:10 +00:00
|
|
|
|
catalog[PdfName.ViewerPreferences][
|
|
|
|
|
PdfName.NonFullScreenPageMode
|
|
|
|
|
] = PdfName.UseOutlines
|
2015-05-10 12:35:09 +00:00
|
|
|
|
# this setting might be overwritten later if fullscreen
|
|
|
|
|
catalog[PdfName.PageMode] = PdfName.UseOutlines
|
|
|
|
|
elif self.panes in [PageMode.none, None]:
|
|
|
|
|
pass
|
|
|
|
|
else:
|
2016-02-13 08:31:33 +00:00
|
|
|
|
raise ValueError("unknown page mode: %s" % self.panes)
|
2015-05-10 12:35:09 +00:00
|
|
|
|
|
|
|
|
|
if self.fit_window:
|
|
|
|
|
catalog[PdfName.ViewerPreferences][PdfName.FitWindow] = TrueObject
|
|
|
|
|
|
|
|
|
|
if self.center_window:
|
2018-12-25 11:30:10 +00:00
|
|
|
|
catalog[PdfName.ViewerPreferences][PdfName.CenterWindow] = TrueObject
|
2015-05-10 12:35:09 +00:00
|
|
|
|
|
|
|
|
|
if self.fullscreen:
|
|
|
|
|
catalog[PdfName.PageMode] = PdfName.FullScreen
|
|
|
|
|
|
|
|
|
|
# see table 8.2 in section 8.2.1 in
|
|
|
|
|
# http://partners.adobe.com/public/developer/en/pdf/PDFReference16.pdf
|
|
|
|
|
# Fit - Fits the page to the window.
|
|
|
|
|
# FitH - Fits the width of the page to the window.
|
|
|
|
|
# FitV - Fits the height of the page to the window.
|
|
|
|
|
# FitR - Fits the rectangle specified by the four coordinates to the
|
|
|
|
|
# window.
|
|
|
|
|
# FitB - Fits the page bounding box to the window. This basically
|
|
|
|
|
# reduces the amount of whitespace (margins) that is displayed
|
|
|
|
|
# and thus focussing more on the text content.
|
|
|
|
|
# FitBH - Fits the width of the page bounding box to the window.
|
|
|
|
|
# FitBV - Fits the height of the page bounding box to the window.
|
|
|
|
|
|
|
|
|
|
# by default the initial page is the first one
|
2020-05-31 15:18:02 +00:00
|
|
|
|
if self.engine == Engine.pikepdf:
|
|
|
|
|
initial_page = self.writer.pages[0]
|
|
|
|
|
else:
|
|
|
|
|
initial_page = self.writer.pagearray[0]
|
2015-05-10 12:35:09 +00:00
|
|
|
|
# we set the open action here to make sure we open on the requested
|
|
|
|
|
# initial page but this value might be overwritten by a custom open
|
2016-06-10 14:39:45 +00:00
|
|
|
|
# action later while still taking the requested initial page into
|
2015-05-10 12:35:09 +00:00
|
|
|
|
# account
|
|
|
|
|
if self.initial_page is not None:
|
2020-05-31 15:18:02 +00:00
|
|
|
|
if self.engine == Engine.pikepdf:
|
|
|
|
|
initial_page = self.writer.pages[self.initial_page - 1]
|
|
|
|
|
else:
|
|
|
|
|
initial_page = self.writer.pagearray[self.initial_page - 1]
|
2018-12-25 11:30:10 +00:00
|
|
|
|
catalog[PdfName.OpenAction] = PdfArray(
|
|
|
|
|
[initial_page, PdfName.XYZ, NullObject, NullObject, 0]
|
|
|
|
|
)
|
2015-05-10 12:35:09 +00:00
|
|
|
|
|
|
|
|
|
if self.magnification == Magnification.fit:
|
|
|
|
|
catalog[PdfName.OpenAction] = PdfArray([initial_page, PdfName.Fit])
|
|
|
|
|
elif self.magnification == Magnification.fith:
|
|
|
|
|
pagewidth = initial_page[PdfName.MediaBox][2]
|
|
|
|
|
catalog[PdfName.OpenAction] = PdfArray(
|
2018-12-25 11:30:10 +00:00
|
|
|
|
[initial_page, PdfName.FitH, pagewidth]
|
|
|
|
|
)
|
2015-05-10 12:35:09 +00:00
|
|
|
|
elif self.magnification == Magnification.fitbh:
|
|
|
|
|
# quick hack to determine the image width on the page
|
|
|
|
|
imgwidth = float(initial_page[PdfName.Contents].stream.split()[4])
|
|
|
|
|
catalog[PdfName.OpenAction] = PdfArray(
|
2018-12-25 11:30:10 +00:00
|
|
|
|
[initial_page, PdfName.FitBH, imgwidth]
|
|
|
|
|
)
|
2015-05-10 12:35:09 +00:00
|
|
|
|
elif isinstance(self.magnification, float):
|
|
|
|
|
catalog[PdfName.OpenAction] = PdfArray(
|
2018-12-25 11:30:10 +00:00
|
|
|
|
[initial_page, PdfName.XYZ, NullObject, NullObject, self.magnification]
|
|
|
|
|
)
|
2015-05-10 12:35:09 +00:00
|
|
|
|
elif self.magnification is None:
|
|
|
|
|
pass
|
|
|
|
|
else:
|
2016-02-13 08:31:33 +00:00
|
|
|
|
raise ValueError("unknown magnification: %s" % self.magnification)
|
2015-05-10 12:35:09 +00:00
|
|
|
|
|
|
|
|
|
if self.page_layout == PageLayout.single:
|
|
|
|
|
catalog[PdfName.PageLayout] = PdfName.SinglePage
|
|
|
|
|
elif self.page_layout == PageLayout.onecolumn:
|
|
|
|
|
catalog[PdfName.PageLayout] = PdfName.OneColumn
|
|
|
|
|
elif self.page_layout == PageLayout.twocolumnright:
|
|
|
|
|
catalog[PdfName.PageLayout] = PdfName.TwoColumnRight
|
|
|
|
|
elif self.page_layout == PageLayout.twocolumnleft:
|
|
|
|
|
catalog[PdfName.PageLayout] = PdfName.TwoColumnLeft
|
|
|
|
|
elif self.page_layout is None:
|
|
|
|
|
pass
|
|
|
|
|
else:
|
2016-02-13 08:31:33 +00:00
|
|
|
|
raise ValueError("unknown page layout: %s" % self.page_layout)
|
2015-05-10 12:35:09 +00:00
|
|
|
|
|
2020-06-01 00:10:07 +00:00
|
|
|
|
if self.pdfa is not None:
|
|
|
|
|
if self.engine == Engine.pikepdf:
|
|
|
|
|
metadata = self.writer.make_stream(self.xmp)
|
|
|
|
|
else:
|
|
|
|
|
metadata = PdfDict(stream=convert_load(self.xmp))
|
|
|
|
|
metadata[PdfName.Subtype] = PdfName.XML
|
|
|
|
|
metadata[PdfName.Type] = PdfName.Metadata
|
|
|
|
|
with open(self.pdfa, "rb") as f:
|
|
|
|
|
icc = f.read()
|
|
|
|
|
intents = PdfDict()
|
|
|
|
|
if self.engine == Engine.pikepdf:
|
|
|
|
|
iccstream = self.writer.make_stream(icc)
|
|
|
|
|
iccstream.stream_dict.N = 3
|
|
|
|
|
else:
|
|
|
|
|
iccstream = PdfDict(stream=convert_load(zlib.compress(icc)))
|
|
|
|
|
iccstream[PdfName.N] = 3
|
|
|
|
|
iccstream[PdfName.Filter] = PdfName.FlateDecode
|
|
|
|
|
intents[PdfName.S] = PdfName.GTS_PDFA1
|
|
|
|
|
intents[PdfName.Type] = PdfName.OutputIntent
|
|
|
|
|
intents[PdfName.OutputConditionIdentifier] = (
|
|
|
|
|
b"sRGB" if self.engine == Engine.pikepdf else b"(sRGB)"
|
|
|
|
|
)
|
|
|
|
|
intents[PdfName.DestOutputProfile] = iccstream
|
|
|
|
|
catalog[PdfName.OutputIntents] = PdfArray([intents])
|
|
|
|
|
catalog[PdfName.Metadata] = metadata
|
|
|
|
|
|
|
|
|
|
if self.engine == Engine.internal:
|
|
|
|
|
self.writer.addobj(metadata)
|
|
|
|
|
self.writer.addobj(iccstream)
|
|
|
|
|
|
2015-05-10 12:35:09 +00:00
|
|
|
|
# now write out the PDF
|
2020-05-31 15:18:02 +00:00
|
|
|
|
if self.engine == Engine.pikepdf:
|
2020-06-01 00:10:07 +00:00
|
|
|
|
self.writer.save(
|
|
|
|
|
outputstream, min_version=self.output_version, linearize=True
|
|
|
|
|
)
|
2020-05-31 15:18:02 +00:00
|
|
|
|
elif self.engine == Engine.pdfrw:
|
|
|
|
|
self.writer.trailer.Info = self.writer.docinfo
|
|
|
|
|
# setting the version attribute of the pdfrw PdfWriter object will
|
|
|
|
|
# influence the behaviour of the write() function
|
|
|
|
|
self.writer.version = self.output_version
|
2020-06-01 00:10:07 +00:00
|
|
|
|
if self.pdfa:
|
|
|
|
|
md5 = hashlib.md5(b"").hexdigest().encode("ascii")
|
|
|
|
|
self.writer.trailer[PdfName.ID] = PdfArray([md5, md5])
|
2016-02-13 07:51:20 +00:00
|
|
|
|
self.writer.write(outputstream)
|
2020-05-28 10:46:37 +00:00
|
|
|
|
elif self.engine == Engine.internal:
|
2020-06-01 00:10:07 +00:00
|
|
|
|
self.writer.tostream(
|
|
|
|
|
self.writer.docinfo,
|
|
|
|
|
outputstream,
|
|
|
|
|
self.output_version,
|
|
|
|
|
None if self.pdfa is None else b"",
|
|
|
|
|
)
|
2020-05-28 10:46:37 +00:00
|
|
|
|
else:
|
|
|
|
|
raise ValueError("unknown engine: %s" % self.engine)
|
2015-05-10 12:35:09 +00:00
|
|
|
|
|
|
|
|
|
|
2021-05-14 09:30:23 +00:00
|
|
|
|
def get_imgmetadata(
|
|
|
|
|
imgdata, imgformat, default_dpi, colorspace, rawdata=None, rotreq=None
|
|
|
|
|
):
|
2018-12-25 11:30:10 +00:00
|
|
|
|
if imgformat == ImageFormat.JPEG2000 and rawdata is not None and imgdata is None:
|
2016-02-17 19:31:46 +00:00
|
|
|
|
# this codepath gets called if the PIL installation is not able to
|
|
|
|
|
# handle JPEG2000 files
|
|
|
|
|
imgwidthpx, imgheightpx, ics, hdpi, vdpi = parsejp2(rawdata)
|
|
|
|
|
|
|
|
|
|
if hdpi is None:
|
|
|
|
|
hdpi = default_dpi
|
|
|
|
|
if vdpi is None:
|
|
|
|
|
vdpi = default_dpi
|
|
|
|
|
ndpi = (hdpi, vdpi)
|
2016-02-16 23:30:15 +00:00
|
|
|
|
else:
|
|
|
|
|
imgwidthpx, imgheightpx = imgdata.size
|
|
|
|
|
|
|
|
|
|
ndpi = imgdata.info.get("dpi", (default_dpi, default_dpi))
|
|
|
|
|
# In python3, the returned dpi value for some tiff images will
|
|
|
|
|
# not be an integer but a float. To make the behaviour of
|
|
|
|
|
# img2pdf the same between python2 and python3, we convert that
|
|
|
|
|
# float into an integer by rounding.
|
|
|
|
|
# Search online for the 72.009 dpi problem for more info.
|
|
|
|
|
ndpi = (int(round(ndpi[0])), int(round(ndpi[1])))
|
|
|
|
|
ics = imgdata.mode
|
2021-06-12 20:03:56 +00:00
|
|
|
|
|
|
|
|
|
if imgformat == ImageFormat.PNG and ics == "RGBA":
|
|
|
|
|
logger.warning("Image contains an alpha channel which will be stored as a separate soft mask (/SMask) image in PDF.")
|
|
|
|
|
elif (ics in ["LA", "PA", "RGBA"] or "transparency" in imgdata.info):
|
2021-04-12 12:12:14 +00:00
|
|
|
|
logger.warning("Image contains transparency which cannot be retained in PDF.")
|
|
|
|
|
logger.warning("img2pdf will not perform a lossy operation.")
|
|
|
|
|
logger.warning("You can remove the alpha channel using imagemagick:")
|
|
|
|
|
logger.warning(
|
2018-12-25 11:30:10 +00:00
|
|
|
|
" $ convert input.png -background white -alpha "
|
|
|
|
|
"remove -alpha off output.png"
|
|
|
|
|
)
|
2019-03-28 09:26:46 +00:00
|
|
|
|
raise AlphaChannelError("Refusing to work on images with alpha channel")
|
2018-03-27 08:39:24 +00:00
|
|
|
|
|
2017-10-18 08:34:59 +00:00
|
|
|
|
# Since commit 07a96209597c5e8dfe785c757d7051ce67a980fb or release 4.1.0
|
|
|
|
|
# Pillow retrieves the DPI from EXIF if it cannot find the DPI in the JPEG
|
|
|
|
|
# header. In that case it can happen that the horizontal and vertical DPI
|
|
|
|
|
# are set to zero.
|
|
|
|
|
if ndpi == (0, 0):
|
|
|
|
|
ndpi = (default_dpi, default_dpi)
|
|
|
|
|
|
2018-08-20 08:24:31 +00:00
|
|
|
|
# PIL defaults to a dpi of 1 if a TIFF image does not specify the dpi.
|
|
|
|
|
# In that case, we want to use a different default.
|
|
|
|
|
if ndpi == (1, 1) and imgformat == ImageFormat.TIFF:
|
2018-12-25 11:30:10 +00:00
|
|
|
|
ndpi = (
|
|
|
|
|
imgdata.tag_v2.get(TiffImagePlugin.X_RESOLUTION, default_dpi),
|
|
|
|
|
imgdata.tag_v2.get(TiffImagePlugin.Y_RESOLUTION, default_dpi),
|
|
|
|
|
)
|
2018-08-20 08:24:31 +00:00
|
|
|
|
|
2021-04-12 12:12:14 +00:00
|
|
|
|
logger.debug("input dpi = %d x %d", *ndpi)
|
2016-02-16 23:30:15 +00:00
|
|
|
|
|
2018-12-25 00:45:38 +00:00
|
|
|
|
rotation = 0
|
2021-05-14 09:30:23 +00:00
|
|
|
|
if rotreq in (None, Rotation.auto, Rotation.ifvalid):
|
|
|
|
|
if hasattr(imgdata, "_getexif") and imgdata._getexif() is not None:
|
|
|
|
|
for tag, value in imgdata._getexif().items():
|
|
|
|
|
if TAGS.get(tag, tag) == "Orientation":
|
|
|
|
|
# Detailed information on EXIF rotation tags:
|
|
|
|
|
# http://impulseadventure.com/photo/exif-orientation.html
|
|
|
|
|
if value == 1:
|
|
|
|
|
rotation = 0
|
|
|
|
|
elif value == 6:
|
|
|
|
|
rotation = 90
|
|
|
|
|
elif value == 3:
|
|
|
|
|
rotation = 180
|
|
|
|
|
elif value == 8:
|
|
|
|
|
rotation = 270
|
|
|
|
|
elif value in (2, 4, 5, 7):
|
|
|
|
|
if rotreq == Rotation.ifvalid:
|
|
|
|
|
logger.warning(
|
|
|
|
|
"Unsupported flipped rotation mode (%d)", value
|
|
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
raise ExifOrientationError(
|
|
|
|
|
"Unsupported flipped rotation mode (%d)" % value
|
|
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
if rotreq == Rotation.ifvalid:
|
|
|
|
|
logger.warning("Invalid rotation (%d)", value)
|
|
|
|
|
else:
|
|
|
|
|
raise ExifOrientationError("Invalid rotation (%d)" % value)
|
|
|
|
|
elif rotreq in (Rotation.none, Rotation["0"]):
|
|
|
|
|
rotation = 0
|
|
|
|
|
elif rotreq == Rotation["90"]:
|
|
|
|
|
rotation = 90
|
|
|
|
|
elif rotreq == Rotation["180"]:
|
|
|
|
|
rotation = 180
|
|
|
|
|
elif rotreq == Rotation["270"]:
|
|
|
|
|
rotation = 270
|
|
|
|
|
else:
|
|
|
|
|
raise Exception("invalid rotreq")
|
2018-12-25 00:45:38 +00:00
|
|
|
|
|
2021-04-12 12:12:14 +00:00
|
|
|
|
logger.debug("rotation = %d°", rotation)
|
2018-12-25 00:45:38 +00:00
|
|
|
|
|
2016-02-16 23:30:15 +00:00
|
|
|
|
if colorspace:
|
|
|
|
|
color = colorspace
|
2021-04-12 12:12:14 +00:00
|
|
|
|
logger.debug("input colorspace (forced) = %s", color)
|
2016-02-16 23:30:15 +00:00
|
|
|
|
else:
|
|
|
|
|
color = None
|
|
|
|
|
for c in Colorspace:
|
|
|
|
|
if c.name == ics:
|
|
|
|
|
color = c
|
|
|
|
|
if color is None:
|
2018-08-17 10:57:46 +00:00
|
|
|
|
# PIL does not provide the information about the original
|
|
|
|
|
# colorspace for 16bit grayscale PNG images. Thus, we retrieve
|
|
|
|
|
# that info manually by looking at byte 10 in the IHDR chunk. We
|
|
|
|
|
# know where to find that in the file because the IHDR chunk must
|
|
|
|
|
# be the first chunk
|
2018-12-25 11:30:10 +00:00
|
|
|
|
if (
|
|
|
|
|
rawdata is not None
|
|
|
|
|
and imgformat == ImageFormat.PNG
|
|
|
|
|
and rawdata[25] == 0
|
|
|
|
|
):
|
2018-08-17 10:57:46 +00:00
|
|
|
|
color = Colorspace.L
|
|
|
|
|
else:
|
|
|
|
|
raise ValueError("unknown colorspace")
|
2016-02-16 23:30:15 +00:00
|
|
|
|
if color == Colorspace.CMYK and imgformat == ImageFormat.JPEG:
|
|
|
|
|
# Adobe inverts CMYK JPEGs for some reason, and others
|
|
|
|
|
# have followed suit as well. Some software assumes the
|
|
|
|
|
# JPEG is inverted if the Adobe tag (APP14), while other
|
|
|
|
|
# software assumes all CMYK JPEGs are inverted. I don't
|
|
|
|
|
# have enough experience with these to know which is
|
|
|
|
|
# better for images currently in the wild, so I'm going
|
|
|
|
|
# with the first approach for now.
|
|
|
|
|
if "adobe" in imgdata.info:
|
2018-12-25 11:30:10 +00:00
|
|
|
|
color = Colorspace["CMYK;I"]
|
2021-04-12 12:12:14 +00:00
|
|
|
|
logger.debug("input colorspace = %s", color.name)
|
2016-02-16 23:30:15 +00:00
|
|
|
|
|
2020-08-06 22:13:53 +00:00
|
|
|
|
iccp = None
|
|
|
|
|
if "icc_profile" in imgdata.info:
|
|
|
|
|
iccp = imgdata.info.get("icc_profile")
|
|
|
|
|
|
2021-04-12 12:12:14 +00:00
|
|
|
|
logger.debug("width x height = %dpx x %dpx", imgwidthpx, imgheightpx)
|
2016-02-16 23:30:15 +00:00
|
|
|
|
|
2020-08-06 22:13:53 +00:00
|
|
|
|
return (color, ndpi, imgwidthpx, imgheightpx, rotation, iccp)
|
2016-02-16 23:30:15 +00:00
|
|
|
|
|
|
|
|
|
|
2018-08-01 20:12:40 +00:00
|
|
|
|
def ccitt_payload_location_from_pil(img):
|
|
|
|
|
# If Pillow is passed an invalid compression argument it will ignore it;
|
|
|
|
|
# make sure the image actually got compressed.
|
2018-12-25 11:30:10 +00:00
|
|
|
|
if img.info["compression"] != "group4":
|
|
|
|
|
raise ValueError(
|
|
|
|
|
"Image not compressed with CCITT Group 4 but with: %s"
|
|
|
|
|
% img.info["compression"]
|
|
|
|
|
)
|
2018-08-01 20:12:40 +00:00
|
|
|
|
|
|
|
|
|
# Read the TIFF tags to find the offset(s) of the compressed data strips.
|
|
|
|
|
strip_offsets = img.tag_v2[TiffImagePlugin.STRIPOFFSETS]
|
|
|
|
|
strip_bytes = img.tag_v2[TiffImagePlugin.STRIPBYTECOUNTS]
|
2018-12-25 11:30:10 +00:00
|
|
|
|
rows_per_strip = img.tag_v2.get(TiffImagePlugin.ROWSPERSTRIP, 2 ** 32 - 1)
|
2018-08-01 20:12:40 +00:00
|
|
|
|
|
|
|
|
|
# PIL always seems to create a single strip even for very large TIFFs when
|
|
|
|
|
# it saves images, so assume we only have to read a single strip.
|
|
|
|
|
# A test ~10 GPixel image was still encoded as a single strip. Just to be
|
|
|
|
|
# safe check throw an error if there is more than one offset.
|
|
|
|
|
if len(strip_offsets) != 1 or len(strip_bytes) != 1:
|
|
|
|
|
raise NotImplementedError("Transcoding multiple strips not supported")
|
|
|
|
|
|
2018-12-25 11:30:10 +00:00
|
|
|
|
(offset,), (length,) = strip_offsets, strip_bytes
|
2018-08-01 20:12:40 +00:00
|
|
|
|
|
2021-04-12 12:12:14 +00:00
|
|
|
|
logger.debug("TIFF strip_offsets: %d" % offset)
|
|
|
|
|
logger.debug("TIFF strip_bytes: %d" % length)
|
2018-08-15 22:11:07 +00:00
|
|
|
|
|
2018-08-01 20:12:40 +00:00
|
|
|
|
return offset, length
|
|
|
|
|
|
|
|
|
|
|
2016-07-21 21:42:48 +00:00
|
|
|
|
def transcode_monochrome(imgdata):
|
|
|
|
|
"""Convert the open PIL.Image imgdata to compressed CCITT Group4 data"""
|
|
|
|
|
|
2021-04-12 12:12:14 +00:00
|
|
|
|
logger.debug("Converting monochrome to CCITT Group4")
|
2016-07-21 21:42:48 +00:00
|
|
|
|
|
|
|
|
|
# Convert the image to Group 4 in memory. If libtiff is not installed and
|
|
|
|
|
# Pillow is not compiled against it, .save() will raise an exception.
|
|
|
|
|
newimgio = BytesIO()
|
2018-07-17 22:58:17 +00:00
|
|
|
|
|
|
|
|
|
# we create a whole new PIL image or otherwise it might happen with some
|
|
|
|
|
# input images, that libtiff fails an assert and the whole process is
|
|
|
|
|
# killed by a SIGABRT:
|
|
|
|
|
# https://gitlab.mister-muffin.de/josch/img2pdf/issues/46
|
|
|
|
|
im = Image.frombytes(imgdata.mode, imgdata.size, imgdata.tobytes())
|
2018-12-25 11:30:10 +00:00
|
|
|
|
im.save(newimgio, format="TIFF", compression="group4")
|
2016-07-21 21:42:48 +00:00
|
|
|
|
|
|
|
|
|
# Open new image in memory
|
|
|
|
|
newimgio.seek(0)
|
|
|
|
|
newimg = Image.open(newimgio)
|
|
|
|
|
|
2018-08-01 20:12:40 +00:00
|
|
|
|
offset, length = ccitt_payload_location_from_pil(newimg)
|
2016-07-21 21:42:48 +00:00
|
|
|
|
|
2018-08-01 20:12:40 +00:00
|
|
|
|
newimgio.seek(offset)
|
|
|
|
|
return newimgio.read(length)
|
2016-07-21 21:42:48 +00:00
|
|
|
|
|
|
|
|
|
|
2018-03-15 10:31:36 +00:00
|
|
|
|
def parse_png(rawdata):
|
|
|
|
|
pngidat = b""
|
2020-05-31 15:18:02 +00:00
|
|
|
|
palette = b""
|
2018-03-15 10:31:36 +00:00
|
|
|
|
i = 16
|
|
|
|
|
while i < len(rawdata):
|
|
|
|
|
# once we can require Python >= 3.2 we can use int.from_bytes() instead
|
2020-08-05 06:13:52 +00:00
|
|
|
|
(n,) = struct.unpack(">I", rawdata[i - 8 : i - 4])
|
2018-03-15 10:31:36 +00:00
|
|
|
|
if i + n > len(rawdata):
|
2018-08-01 20:28:44 +00:00
|
|
|
|
raise Exception("invalid png: %d %d %d" % (i, n, len(rawdata)))
|
2018-12-25 11:30:10 +00:00
|
|
|
|
if rawdata[i - 4 : i] == b"IDAT":
|
|
|
|
|
pngidat += rawdata[i : i + n]
|
|
|
|
|
elif rawdata[i - 4 : i] == b"PLTE":
|
2020-05-31 15:18:02 +00:00
|
|
|
|
palette += rawdata[i : i + n]
|
2018-03-15 10:31:36 +00:00
|
|
|
|
i += n
|
|
|
|
|
i += 12
|
|
|
|
|
return pngidat, palette
|
2016-07-21 21:42:48 +00:00
|
|
|
|
|
2018-08-01 20:28:44 +00:00
|
|
|
|
|
2021-05-14 09:30:23 +00:00
|
|
|
|
def read_images(rawdata, colorspace, first_frame_only=False, rot=None):
|
2015-05-10 12:35:09 +00:00
|
|
|
|
im = BytesIO(rawdata)
|
|
|
|
|
im.seek(0)
|
2016-02-17 19:31:46 +00:00
|
|
|
|
imgdata = None
|
2015-05-10 12:35:09 +00:00
|
|
|
|
try:
|
|
|
|
|
imgdata = Image.open(im)
|
|
|
|
|
except IOError as e:
|
|
|
|
|
# test if it is a jpeg2000 image
|
2017-08-20 05:04:16 +00:00
|
|
|
|
if rawdata[:12] != b"\x00\x00\x00\x0C\x6A\x50\x20\x20\x0D\x0A\x87\x0A":
|
2018-12-25 11:30:10 +00:00
|
|
|
|
raise ImageOpenError(
|
|
|
|
|
"cannot read input image (not jpeg2000). "
|
|
|
|
|
"PIL: error reading image: %s" % e
|
|
|
|
|
)
|
2015-05-10 12:35:09 +00:00
|
|
|
|
# image is jpeg2000
|
|
|
|
|
imgformat = ImageFormat.JPEG2000
|
|
|
|
|
else:
|
2021-04-12 12:13:19 +00:00
|
|
|
|
logger.debug("PIL format = %s", imgdata.format)
|
2015-05-10 12:35:09 +00:00
|
|
|
|
imgformat = None
|
|
|
|
|
for f in ImageFormat:
|
|
|
|
|
if f.name == imgdata.format:
|
|
|
|
|
imgformat = f
|
|
|
|
|
if imgformat is None:
|
2016-02-17 17:46:24 +00:00
|
|
|
|
imgformat = ImageFormat.other
|
2015-05-10 12:35:09 +00:00
|
|
|
|
|
2021-04-14 06:25:18 +00:00
|
|
|
|
def cleanup():
|
|
|
|
|
if imgdata is not None:
|
|
|
|
|
# the python-pil version 2.3.0-1ubuntu3 in Ubuntu does not have the
|
|
|
|
|
# close() method
|
|
|
|
|
try:
|
|
|
|
|
imgdata.close()
|
|
|
|
|
except AttributeError:
|
|
|
|
|
pass
|
|
|
|
|
im.close()
|
|
|
|
|
|
2021-04-12 12:12:14 +00:00
|
|
|
|
logger.debug("imgformat = %s", imgformat.name)
|
2015-05-10 12:35:09 +00:00
|
|
|
|
|
|
|
|
|
# depending on the input format, determine whether to pass the raw
|
|
|
|
|
# image or the zlib compressed color information
|
2018-03-24 17:59:02 +00:00
|
|
|
|
|
|
|
|
|
# JPEG and JPEG2000 can be embedded into the PDF as-is
|
2015-05-10 12:35:09 +00:00
|
|
|
|
if imgformat == ImageFormat.JPEG or imgformat == ImageFormat.JPEG2000:
|
2020-08-06 22:13:53 +00:00
|
|
|
|
color, ndpi, imgwidthpx, imgheightpx, rotation, iccp = get_imgmetadata(
|
2021-05-14 09:30:23 +00:00
|
|
|
|
imgdata, imgformat, default_dpi, colorspace, rawdata, rot
|
2018-12-25 11:30:10 +00:00
|
|
|
|
)
|
|
|
|
|
if color == Colorspace["1"]:
|
2016-02-16 23:37:13 +00:00
|
|
|
|
raise JpegColorspaceError("jpeg can't be monochrome")
|
2018-12-25 11:30:10 +00:00
|
|
|
|
if color == Colorspace["P"]:
|
2016-02-16 23:37:13 +00:00
|
|
|
|
raise JpegColorspaceError("jpeg can't have a color palette")
|
2018-12-25 11:30:10 +00:00
|
|
|
|
if color == Colorspace["RGBA"]:
|
2016-02-16 23:37:13 +00:00
|
|
|
|
raise JpegColorspaceError("jpeg can't have an alpha channel")
|
2021-04-12 12:12:14 +00:00
|
|
|
|
logger.debug("read_images() embeds a JPEG")
|
2021-04-14 06:25:18 +00:00
|
|
|
|
cleanup()
|
2018-12-25 11:30:10 +00:00
|
|
|
|
return [
|
|
|
|
|
(
|
|
|
|
|
color,
|
|
|
|
|
ndpi,
|
|
|
|
|
imgformat,
|
|
|
|
|
rawdata,
|
2021-06-12 20:03:56 +00:00
|
|
|
|
None,
|
2018-12-25 11:30:10 +00:00
|
|
|
|
imgwidthpx,
|
|
|
|
|
imgheightpx,
|
|
|
|
|
[],
|
|
|
|
|
False,
|
|
|
|
|
8,
|
|
|
|
|
rotation,
|
2020-08-06 22:13:53 +00:00
|
|
|
|
iccp,
|
2018-12-25 11:30:10 +00:00
|
|
|
|
)
|
|
|
|
|
]
|
2018-03-24 17:59:02 +00:00
|
|
|
|
|
2021-04-12 20:57:51 +00:00
|
|
|
|
# The MPO format is multiple JPEG images concatenated together
|
|
|
|
|
# we use the offset and size information to dissect the MPO into its
|
|
|
|
|
# individual JPEG images and then embed those into the PDF individually.
|
|
|
|
|
#
|
|
|
|
|
# The downside is, that this truncates the first JPEG as the MPO metadata
|
|
|
|
|
# will still be in it but the referenced images are chopped off. We still
|
|
|
|
|
# do it that way instead of adding the full MPO as the first image to not
|
|
|
|
|
# store duplicate image data.
|
|
|
|
|
if imgformat == ImageFormat.MPO:
|
|
|
|
|
result = []
|
|
|
|
|
img_page_count = 0
|
|
|
|
|
for offset, mpent in zip(
|
|
|
|
|
imgdata._MpoImageFile__mpoffsets, imgdata.mpinfo[0xB002]
|
|
|
|
|
):
|
|
|
|
|
if first_frame_only and img_page_count > 0:
|
|
|
|
|
break
|
2021-04-28 13:37:44 +00:00
|
|
|
|
with BytesIO(rawdata[offset : offset + mpent["Size"]]) as rawframe:
|
|
|
|
|
with Image.open(rawframe) as imframe:
|
|
|
|
|
# The first frame contains the data that makes the JPEG a MPO
|
|
|
|
|
# Could we thus embed an MPO into another MPO? Lets not support
|
|
|
|
|
# such madness ;)
|
|
|
|
|
if img_page_count > 0 and imframe.format != "JPEG":
|
|
|
|
|
raise Exception("MPO payload must be a JPEG %s", imframe.format)
|
|
|
|
|
(
|
|
|
|
|
color,
|
|
|
|
|
ndpi,
|
|
|
|
|
imgwidthpx,
|
|
|
|
|
imgheightpx,
|
|
|
|
|
rotation,
|
|
|
|
|
iccp,
|
|
|
|
|
) = get_imgmetadata(
|
2021-05-14 09:30:23 +00:00
|
|
|
|
imframe, ImageFormat.JPEG, default_dpi, colorspace, rotreq=rot
|
2021-04-28 13:37:44 +00:00
|
|
|
|
)
|
2021-04-12 20:57:51 +00:00
|
|
|
|
if color == Colorspace["1"]:
|
|
|
|
|
raise JpegColorspaceError("jpeg can't be monochrome")
|
|
|
|
|
if color == Colorspace["P"]:
|
|
|
|
|
raise JpegColorspaceError("jpeg can't have a color palette")
|
|
|
|
|
if color == Colorspace["RGBA"]:
|
|
|
|
|
raise JpegColorspaceError("jpeg can't have an alpha channel")
|
|
|
|
|
logger.debug("read_images() embeds a JPEG from MPO")
|
|
|
|
|
result.append(
|
|
|
|
|
(
|
|
|
|
|
color,
|
|
|
|
|
ndpi,
|
|
|
|
|
ImageFormat.JPEG,
|
|
|
|
|
rawdata[offset : offset + mpent["Size"]],
|
2021-06-12 20:03:56 +00:00
|
|
|
|
None,
|
2021-04-12 20:57:51 +00:00
|
|
|
|
imgwidthpx,
|
|
|
|
|
imgheightpx,
|
|
|
|
|
[],
|
|
|
|
|
False,
|
|
|
|
|
8,
|
|
|
|
|
rotation,
|
|
|
|
|
iccp,
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
img_page_count += 1
|
2021-04-14 06:25:18 +00:00
|
|
|
|
cleanup()
|
2021-04-12 20:57:51 +00:00
|
|
|
|
return result
|
2021-06-12 20:03:56 +00:00
|
|
|
|
|
2018-03-24 17:59:02 +00:00
|
|
|
|
# We can directly embed the IDAT chunk of PNG images if the PNG is not
|
|
|
|
|
# interlaced
|
|
|
|
|
#
|
|
|
|
|
# PIL does not provide the information whether a PNG was stored interlaced
|
|
|
|
|
# or not. Thus, we retrieve that info manually by looking at byte 13 in the
|
|
|
|
|
# IHDR chunk. We know where to find that in the file because the IHDR chunk
|
|
|
|
|
# must be the first chunk.
|
2021-06-12 20:03:56 +00:00
|
|
|
|
if imgformat == ImageFormat.PNG:
|
2020-08-06 22:13:53 +00:00
|
|
|
|
color, ndpi, imgwidthpx, imgheightpx, rotation, iccp = get_imgmetadata(
|
2021-05-14 09:30:23 +00:00
|
|
|
|
imgdata, imgformat, default_dpi, colorspace, rawdata, rot
|
2018-12-25 11:30:10 +00:00
|
|
|
|
)
|
2021-06-12 20:03:56 +00:00
|
|
|
|
|
|
|
|
|
if color == Colorspace.RGBA or rawdata[28] == 0:
|
|
|
|
|
if color == Colorspace.RGBA:
|
|
|
|
|
r, g, b, a = imgdata.split()
|
|
|
|
|
pngdata = zlib.compress(Image.merge("RGB", (r, g, b)).tobytes())
|
|
|
|
|
smaskdata = zlib.compress(a.tobytes())
|
|
|
|
|
palette = None
|
|
|
|
|
else:
|
|
|
|
|
pngdata, palette = parse_png(rawdata)
|
|
|
|
|
smaskdata = None
|
|
|
|
|
|
|
|
|
|
# PIL does not provide the information about the original bits per
|
|
|
|
|
# sample. Thus, we retrieve that info manually by looking at byte 9 in
|
|
|
|
|
# the IHDR chunk. We know where to find that in the file because the
|
|
|
|
|
# IHDR chunk must be the first chunk
|
|
|
|
|
depth = rawdata[24]
|
|
|
|
|
if depth not in [1, 2, 4, 8, 16]:
|
|
|
|
|
raise ValueError("invalid bit depth: %d" % depth)
|
|
|
|
|
logger.debug("read_images() embeds a PNG")
|
|
|
|
|
cleanup()
|
|
|
|
|
return [
|
|
|
|
|
(
|
|
|
|
|
color,
|
|
|
|
|
ndpi,
|
|
|
|
|
imgformat,
|
|
|
|
|
pngdata,
|
|
|
|
|
smaskdata,
|
|
|
|
|
imgwidthpx,
|
|
|
|
|
imgheightpx,
|
|
|
|
|
palette,
|
|
|
|
|
False,
|
|
|
|
|
depth,
|
|
|
|
|
rotation,
|
|
|
|
|
iccp,
|
|
|
|
|
)
|
|
|
|
|
]
|
2018-08-01 20:12:40 +00:00
|
|
|
|
|
2018-08-15 22:23:18 +00:00
|
|
|
|
# If our input is not JPEG or PNG, then we might have a format that
|
|
|
|
|
# supports multiple frames (like TIFF or GIF), so we need a loop to
|
|
|
|
|
# iterate through all frames of the image.
|
|
|
|
|
#
|
|
|
|
|
# Each frame gets compressed using PNG compression *except* if:
|
|
|
|
|
#
|
|
|
|
|
# * The image is monochrome => encode using CCITT group 4
|
|
|
|
|
#
|
|
|
|
|
# * The image is CMYK => zip plain RGB data
|
|
|
|
|
#
|
|
|
|
|
# * We are handling a CCITT encoded TIFF frame => embed data
|
2016-02-16 23:30:15 +00:00
|
|
|
|
|
2018-03-24 17:59:02 +00:00
|
|
|
|
result = []
|
|
|
|
|
img_page_count = 0
|
|
|
|
|
# loop through all frames of the image (example: multipage TIFF)
|
|
|
|
|
while True:
|
|
|
|
|
try:
|
|
|
|
|
imgdata.seek(img_page_count)
|
|
|
|
|
except EOFError:
|
|
|
|
|
break
|
2016-02-16 23:30:15 +00:00
|
|
|
|
|
2018-03-24 17:59:02 +00:00
|
|
|
|
if first_frame_only and img_page_count > 0:
|
|
|
|
|
break
|
2016-02-16 23:30:15 +00:00
|
|
|
|
|
2018-08-20 08:25:20 +00:00
|
|
|
|
# PIL is unable to preserve the data of 16-bit RGB TIFF files and will
|
|
|
|
|
# convert it to 8-bit without the possibility to retrieve the original
|
|
|
|
|
# data
|
|
|
|
|
# https://github.com/python-pillow/Pillow/issues/1888
|
|
|
|
|
#
|
|
|
|
|
# Some tiff images do not have BITSPERSAMPLE set. Use this to create
|
|
|
|
|
# such a tiff: tiffset -u 258 test.tif
|
2018-12-25 11:30:10 +00:00
|
|
|
|
if (
|
|
|
|
|
imgformat == ImageFormat.TIFF
|
|
|
|
|
and max(imgdata.tag_v2.get(TiffImagePlugin.BITSPERSAMPLE, [1])) > 8
|
|
|
|
|
):
|
2018-08-20 08:25:20 +00:00
|
|
|
|
raise ValueError("PIL is unable to preserve more than 8 bits per sample")
|
|
|
|
|
|
2018-08-15 22:23:18 +00:00
|
|
|
|
# We can directly copy the data out of a CCITT Group 4 encoded TIFF, if it
|
|
|
|
|
# only contains a single strip
|
2018-12-25 11:30:10 +00:00
|
|
|
|
if (
|
|
|
|
|
imgformat == ImageFormat.TIFF
|
|
|
|
|
and imgdata.info["compression"] == "group4"
|
|
|
|
|
and len(imgdata.tag_v2[TiffImagePlugin.STRIPOFFSETS]) == 1
|
|
|
|
|
):
|
2018-08-15 22:23:18 +00:00
|
|
|
|
photo = imgdata.tag_v2[TiffImagePlugin.PHOTOMETRIC_INTERPRETATION]
|
|
|
|
|
inverted = False
|
|
|
|
|
if photo == 0:
|
|
|
|
|
inverted = True
|
|
|
|
|
elif photo != 1:
|
2018-12-25 11:30:10 +00:00
|
|
|
|
raise ValueError(
|
|
|
|
|
"unsupported photometric interpretation for "
|
|
|
|
|
"group4 tiff: %d" % photo
|
|
|
|
|
)
|
2020-08-06 22:13:53 +00:00
|
|
|
|
color, ndpi, imgwidthpx, imgheightpx, rotation, iccp = get_imgmetadata(
|
2021-05-14 09:30:23 +00:00
|
|
|
|
imgdata, imgformat, default_dpi, colorspace, rawdata, rot
|
2018-12-25 11:30:10 +00:00
|
|
|
|
)
|
2018-08-15 22:23:18 +00:00
|
|
|
|
offset, length = ccitt_payload_location_from_pil(imgdata)
|
|
|
|
|
im.seek(offset)
|
|
|
|
|
rawdata = im.read(length)
|
|
|
|
|
fillorder = imgdata.tag_v2.get(TiffImagePlugin.FILLORDER)
|
|
|
|
|
if fillorder is None:
|
|
|
|
|
# no FillOrder: nothing to do
|
|
|
|
|
pass
|
|
|
|
|
elif fillorder == 1:
|
|
|
|
|
# msb-to-lsb: nothing to do
|
|
|
|
|
pass
|
|
|
|
|
elif fillorder == 2:
|
2021-04-12 12:12:14 +00:00
|
|
|
|
logger.debug("fillorder is lsb-to-msb => reverse bits")
|
2018-08-15 22:23:18 +00:00
|
|
|
|
# lsb-to-msb: reverse bits of each byte
|
|
|
|
|
rawdata = bytearray(rawdata)
|
|
|
|
|
for i in range(len(rawdata)):
|
|
|
|
|
rawdata[i] = TIFFBitRevTable[rawdata[i]]
|
|
|
|
|
rawdata = bytes(rawdata)
|
|
|
|
|
else:
|
|
|
|
|
raise ValueError("unsupported FillOrder: %d" % fillorder)
|
2021-04-12 12:12:14 +00:00
|
|
|
|
logger.debug("read_images() embeds Group4 from TIFF")
|
2018-12-25 11:30:10 +00:00
|
|
|
|
result.append(
|
|
|
|
|
(
|
|
|
|
|
color,
|
|
|
|
|
ndpi,
|
|
|
|
|
ImageFormat.CCITTGroup4,
|
|
|
|
|
rawdata,
|
2021-06-12 20:03:56 +00:00
|
|
|
|
None,
|
2018-12-25 11:30:10 +00:00
|
|
|
|
imgwidthpx,
|
|
|
|
|
imgheightpx,
|
|
|
|
|
[],
|
|
|
|
|
inverted,
|
|
|
|
|
1,
|
|
|
|
|
rotation,
|
2020-08-06 22:13:53 +00:00
|
|
|
|
iccp,
|
2018-12-25 11:30:10 +00:00
|
|
|
|
)
|
|
|
|
|
)
|
2018-08-15 22:23:18 +00:00
|
|
|
|
img_page_count += 1
|
|
|
|
|
continue
|
|
|
|
|
|
2021-04-12 12:12:14 +00:00
|
|
|
|
logger.debug("Converting frame: %d" % img_page_count)
|
2018-03-24 17:59:02 +00:00
|
|
|
|
|
2020-08-06 22:13:53 +00:00
|
|
|
|
color, ndpi, imgwidthpx, imgheightpx, rotation, iccp = get_imgmetadata(
|
2021-05-14 09:30:23 +00:00
|
|
|
|
imgdata, imgformat, default_dpi, colorspace, rotreq=rot
|
2018-12-25 11:30:10 +00:00
|
|
|
|
)
|
2018-03-24 17:59:02 +00:00
|
|
|
|
|
|
|
|
|
newimg = None
|
2018-12-25 11:30:10 +00:00
|
|
|
|
if color == Colorspace["1"]:
|
2018-03-24 17:59:02 +00:00
|
|
|
|
try:
|
|
|
|
|
ccittdata = transcode_monochrome(imgdata)
|
2021-04-12 12:12:14 +00:00
|
|
|
|
logger.debug("read_images() encoded a B/W image as CCITT group 4")
|
2018-12-25 11:30:10 +00:00
|
|
|
|
result.append(
|
|
|
|
|
(
|
|
|
|
|
color,
|
|
|
|
|
ndpi,
|
|
|
|
|
ImageFormat.CCITTGroup4,
|
|
|
|
|
ccittdata,
|
2021-06-12 20:03:56 +00:00
|
|
|
|
None,
|
2018-12-25 11:30:10 +00:00
|
|
|
|
imgwidthpx,
|
|
|
|
|
imgheightpx,
|
|
|
|
|
[],
|
|
|
|
|
False,
|
|
|
|
|
1,
|
|
|
|
|
rotation,
|
2020-08-06 22:13:53 +00:00
|
|
|
|
iccp,
|
2018-12-25 11:30:10 +00:00
|
|
|
|
)
|
|
|
|
|
)
|
2018-03-24 17:59:02 +00:00
|
|
|
|
img_page_count += 1
|
|
|
|
|
continue
|
|
|
|
|
except Exception as e:
|
2021-04-12 12:12:14 +00:00
|
|
|
|
logger.debug(e)
|
|
|
|
|
logger.debug("Converting colorspace 1 to L")
|
2018-12-25 11:30:10 +00:00
|
|
|
|
newimg = imgdata.convert("L")
|
2018-03-24 17:59:02 +00:00
|
|
|
|
color = Colorspace.L
|
2018-12-25 11:30:10 +00:00
|
|
|
|
elif color in [
|
|
|
|
|
Colorspace.RGB,
|
|
|
|
|
Colorspace.L,
|
|
|
|
|
Colorspace.CMYK,
|
|
|
|
|
Colorspace["CMYK;I"],
|
|
|
|
|
Colorspace.P,
|
|
|
|
|
]:
|
2021-04-12 12:12:14 +00:00
|
|
|
|
logger.debug("Colorspace is OK: %s", color)
|
2018-03-24 17:59:02 +00:00
|
|
|
|
newimg = imgdata
|
|
|
|
|
else:
|
2018-12-25 11:30:10 +00:00
|
|
|
|
raise ValueError("unknown or unsupported colorspace: %s" % color.name)
|
2018-07-17 14:47:27 +00:00
|
|
|
|
# the PNG format does not support CMYK, so we fall back to normal
|
|
|
|
|
# compression
|
|
|
|
|
if color in [Colorspace.CMYK, Colorspace["CMYK;I"]]:
|
|
|
|
|
imggz = zlib.compress(newimg.tobytes())
|
2021-04-12 12:12:14 +00:00
|
|
|
|
logger.debug("read_images() encoded CMYK with flate compression")
|
2018-12-25 11:30:10 +00:00
|
|
|
|
result.append(
|
|
|
|
|
(
|
|
|
|
|
color,
|
|
|
|
|
ndpi,
|
|
|
|
|
imgformat,
|
|
|
|
|
imggz,
|
2021-06-12 20:03:56 +00:00
|
|
|
|
None,
|
2018-12-25 11:30:10 +00:00
|
|
|
|
imgwidthpx,
|
|
|
|
|
imgheightpx,
|
|
|
|
|
[],
|
|
|
|
|
False,
|
|
|
|
|
8,
|
|
|
|
|
rotation,
|
2020-08-06 22:13:53 +00:00
|
|
|
|
iccp,
|
2018-12-25 11:30:10 +00:00
|
|
|
|
)
|
|
|
|
|
)
|
2018-07-17 14:47:27 +00:00
|
|
|
|
else:
|
|
|
|
|
# cheapo version to retrieve a PNG encoding of the payload is to
|
|
|
|
|
# just save it with PIL. In the future this could be replaced by
|
|
|
|
|
# dedicated function applying the Paeth PNG filter to the raw pixel
|
|
|
|
|
pngbuffer = BytesIO()
|
|
|
|
|
newimg.save(pngbuffer, format="png")
|
|
|
|
|
pngidat, palette = parse_png(pngbuffer.getvalue())
|
2018-08-17 10:55:55 +00:00
|
|
|
|
# PIL does not provide the information about the original bits per
|
|
|
|
|
# sample. Thus, we retrieve that info manually by looking at byte 9 in
|
|
|
|
|
# the IHDR chunk. We know where to find that in the file because the
|
|
|
|
|
# IHDR chunk must be the first chunk
|
|
|
|
|
pngbuffer.seek(24)
|
|
|
|
|
depth = ord(pngbuffer.read(1))
|
|
|
|
|
if depth not in [1, 2, 4, 8, 16]:
|
|
|
|
|
raise ValueError("invalid bit depth: %d" % depth)
|
2021-04-12 12:12:14 +00:00
|
|
|
|
logger.debug("read_images() encoded an image as PNG")
|
2018-12-25 11:30:10 +00:00
|
|
|
|
result.append(
|
|
|
|
|
(
|
|
|
|
|
color,
|
|
|
|
|
ndpi,
|
|
|
|
|
ImageFormat.PNG,
|
|
|
|
|
pngidat,
|
2021-06-12 20:03:56 +00:00
|
|
|
|
None,
|
2018-12-25 11:30:10 +00:00
|
|
|
|
imgwidthpx,
|
|
|
|
|
imgheightpx,
|
|
|
|
|
palette,
|
|
|
|
|
False,
|
|
|
|
|
depth,
|
|
|
|
|
rotation,
|
2020-08-06 22:13:53 +00:00
|
|
|
|
iccp,
|
2018-12-25 11:30:10 +00:00
|
|
|
|
)
|
|
|
|
|
)
|
2018-03-24 17:59:02 +00:00
|
|
|
|
img_page_count += 1
|
2021-04-14 06:25:18 +00:00
|
|
|
|
cleanup()
|
2018-03-24 17:59:02 +00:00
|
|
|
|
return result
|
2015-05-10 12:35:09 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# converts a length in pixels to a length in PDF units (1/72 of an inch)
|
|
|
|
|
def px_to_pt(length, dpi):
|
2018-12-25 11:30:10 +00:00
|
|
|
|
return 72.0 * length / dpi
|
2015-05-10 12:35:09 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def cm_to_pt(length):
|
2018-12-25 11:30:10 +00:00
|
|
|
|
return (72.0 * length) / 2.54
|
2015-05-10 12:35:09 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def mm_to_pt(length):
|
2018-12-25 11:30:10 +00:00
|
|
|
|
return (72.0 * length) / 25.4
|
2015-05-10 12:35:09 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def in_to_pt(length):
|
2018-12-25 11:30:10 +00:00
|
|
|
|
return 72.0 * length
|
2015-05-10 12:35:09 +00:00
|
|
|
|
|
|
|
|
|
|
2018-12-25 11:30:10 +00:00
|
|
|
|
def get_layout_fun(
|
|
|
|
|
pagesize=None, imgsize=None, border=None, fit=None, auto_orient=False
|
|
|
|
|
):
|
2015-05-10 12:35:09 +00:00
|
|
|
|
def fitfun(fit, imgwidth, imgheight, fitwidth, fitheight):
|
|
|
|
|
if fitwidth is None and fitheight is None:
|
2016-02-13 08:31:33 +00:00
|
|
|
|
raise ValueError("fitwidth and fitheight cannot both be None")
|
2015-05-10 12:35:09 +00:00
|
|
|
|
# if fit is fill or enlarge then it is okay if one of the dimensions
|
|
|
|
|
# are negative but one of them must still be positive
|
|
|
|
|
# if fit is not fill or enlarge then both dimensions must be positive
|
2018-12-25 11:30:10 +00:00
|
|
|
|
if (
|
|
|
|
|
fit in [FitMode.fill, FitMode.enlarge]
|
|
|
|
|
and fitwidth is not None
|
|
|
|
|
and fitwidth < 0
|
|
|
|
|
and fitheight is not None
|
|
|
|
|
and fitheight < 0
|
|
|
|
|
):
|
|
|
|
|
raise ValueError(
|
2019-03-28 23:34:31 +00:00
|
|
|
|
"cannot fit into a rectangle where both dimensions are negative"
|
2018-12-25 11:30:10 +00:00
|
|
|
|
)
|
|
|
|
|
elif fit not in [FitMode.fill, FitMode.enlarge] and (
|
|
|
|
|
(fitwidth is not None and fitwidth < 0)
|
|
|
|
|
or (fitheight is not None and fitheight < 0)
|
|
|
|
|
):
|
|
|
|
|
raise Exception(
|
2019-03-28 23:34:31 +00:00
|
|
|
|
"cannot fit into a rectangle where either dimensions are negative"
|
2018-12-25 11:30:10 +00:00
|
|
|
|
)
|
2015-05-10 12:35:09 +00:00
|
|
|
|
|
|
|
|
|
def default():
|
|
|
|
|
if fitwidth is not None and fitheight is not None:
|
|
|
|
|
newimgwidth = fitwidth
|
2018-12-25 11:30:10 +00:00
|
|
|
|
newimgheight = (newimgwidth * imgheight) / imgwidth
|
2015-05-10 12:35:09 +00:00
|
|
|
|
if newimgheight > fitheight:
|
|
|
|
|
newimgheight = fitheight
|
2018-12-25 11:30:10 +00:00
|
|
|
|
newimgwidth = (newimgheight * imgwidth) / imgheight
|
2015-05-10 12:35:09 +00:00
|
|
|
|
elif fitwidth is None and fitheight is not None:
|
|
|
|
|
newimgheight = fitheight
|
2018-12-25 11:30:10 +00:00
|
|
|
|
newimgwidth = (newimgheight * imgwidth) / imgheight
|
2015-05-10 12:35:09 +00:00
|
|
|
|
elif fitheight is None and fitwidth is not None:
|
|
|
|
|
newimgwidth = fitwidth
|
2018-12-25 11:30:10 +00:00
|
|
|
|
newimgheight = (newimgwidth * imgheight) / imgwidth
|
2015-02-16 06:39:07 +00:00
|
|
|
|
else:
|
2016-02-13 08:31:33 +00:00
|
|
|
|
raise ValueError("fitwidth and fitheight cannot both be None")
|
2015-05-10 12:35:09 +00:00
|
|
|
|
return newimgwidth, newimgheight
|
2018-12-25 11:30:10 +00:00
|
|
|
|
|
2015-05-10 12:35:09 +00:00
|
|
|
|
if fit is None or fit == FitMode.into:
|
|
|
|
|
return default()
|
|
|
|
|
elif fit == FitMode.fill:
|
|
|
|
|
if fitwidth is not None and fitheight is not None:
|
|
|
|
|
newimgwidth = fitwidth
|
2018-12-25 11:30:10 +00:00
|
|
|
|
newimgheight = (newimgwidth * imgheight) / imgwidth
|
2015-05-10 12:35:09 +00:00
|
|
|
|
if newimgheight < fitheight:
|
|
|
|
|
newimgheight = fitheight
|
2018-12-25 11:30:10 +00:00
|
|
|
|
newimgwidth = (newimgheight * imgwidth) / imgheight
|
2015-05-10 12:35:09 +00:00
|
|
|
|
elif fitwidth is None and fitheight is not None:
|
|
|
|
|
newimgheight = fitheight
|
2018-12-25 11:30:10 +00:00
|
|
|
|
newimgwidth = (newimgheight * imgwidth) / imgheight
|
2015-05-10 12:35:09 +00:00
|
|
|
|
elif fitheight is None and fitwidth is not None:
|
|
|
|
|
newimgwidth = fitwidth
|
2018-12-25 11:30:10 +00:00
|
|
|
|
newimgheight = (newimgwidth * imgheight) / imgwidth
|
2015-05-10 12:35:09 +00:00
|
|
|
|
else:
|
2016-02-13 08:31:33 +00:00
|
|
|
|
raise ValueError("fitwidth and fitheight cannot both be None")
|
2015-05-10 12:35:09 +00:00
|
|
|
|
return newimgwidth, newimgheight
|
|
|
|
|
elif fit == FitMode.exact:
|
|
|
|
|
if fitwidth is not None and fitheight is not None:
|
|
|
|
|
return fitwidth, fitheight
|
|
|
|
|
elif fitwidth is None and fitheight is not None:
|
|
|
|
|
newimgheight = fitheight
|
2018-12-25 11:30:10 +00:00
|
|
|
|
newimgwidth = (newimgheight * imgwidth) / imgheight
|
2015-05-10 12:35:09 +00:00
|
|
|
|
elif fitheight is None and fitwidth is not None:
|
|
|
|
|
newimgwidth = fitwidth
|
2018-12-25 11:30:10 +00:00
|
|
|
|
newimgheight = (newimgwidth * imgheight) / imgwidth
|
2015-05-10 12:35:09 +00:00
|
|
|
|
else:
|
2016-02-13 08:31:33 +00:00
|
|
|
|
raise ValueError("fitwidth and fitheight cannot both be None")
|
2016-02-17 17:11:11 +00:00
|
|
|
|
return newimgwidth, newimgheight
|
2015-05-10 12:35:09 +00:00
|
|
|
|
elif fit == FitMode.shrink:
|
|
|
|
|
if fitwidth is not None and fitheight is not None:
|
|
|
|
|
if imgwidth <= fitwidth and imgheight <= fitheight:
|
|
|
|
|
return imgwidth, imgheight
|
|
|
|
|
elif fitwidth is None and fitheight is not None:
|
|
|
|
|
if imgheight <= fitheight:
|
|
|
|
|
return imgwidth, imgheight
|
|
|
|
|
elif fitheight is None and fitwidth is not None:
|
|
|
|
|
if imgwidth <= fitwidth:
|
|
|
|
|
return imgwidth, imgheight
|
2015-02-16 06:39:07 +00:00
|
|
|
|
else:
|
2016-02-13 08:31:33 +00:00
|
|
|
|
raise ValueError("fitwidth and fitheight cannot both be None")
|
2015-05-10 12:35:09 +00:00
|
|
|
|
return default()
|
|
|
|
|
elif fit == FitMode.enlarge:
|
|
|
|
|
if fitwidth is not None and fitheight is not None:
|
|
|
|
|
if imgwidth > fitwidth or imgheight > fitheight:
|
|
|
|
|
return imgwidth, imgheight
|
|
|
|
|
elif fitwidth is None and fitheight is not None:
|
|
|
|
|
if imgheight > fitheight:
|
|
|
|
|
return imgwidth, imgheight
|
|
|
|
|
elif fitheight is None and fitwidth is not None:
|
|
|
|
|
if imgwidth > fitwidth:
|
|
|
|
|
return imgwidth, imgheight
|
|
|
|
|
else:
|
2016-02-13 08:31:33 +00:00
|
|
|
|
raise ValueError("fitwidth and fitheight cannot both be None")
|
2015-05-10 12:35:09 +00:00
|
|
|
|
return default()
|
2015-02-16 06:39:07 +00:00
|
|
|
|
else:
|
2015-05-10 12:35:09 +00:00
|
|
|
|
raise NotImplementedError
|
2018-12-25 11:30:10 +00:00
|
|
|
|
|
2015-05-10 12:35:09 +00:00
|
|
|
|
# if no layout arguments are given, then the image size is equal to the
|
|
|
|
|
# page size and will be drawn with the default dpi
|
|
|
|
|
if pagesize is None and imgsize is None and border is None:
|
|
|
|
|
return default_layout_fun
|
|
|
|
|
if pagesize is None and imgsize is None and border is not None:
|
2018-12-25 11:30:10 +00:00
|
|
|
|
|
2015-05-10 12:35:09 +00:00
|
|
|
|
def layout_fun(imgwidthpx, imgheightpx, ndpi):
|
|
|
|
|
imgwidthpdf = px_to_pt(imgwidthpx, ndpi[0])
|
|
|
|
|
imgheightpdf = px_to_pt(imgheightpx, ndpi[1])
|
2018-12-25 11:30:10 +00:00
|
|
|
|
pagewidth = imgwidthpdf + 2 * border[1]
|
|
|
|
|
pageheight = imgheightpdf + 2 * border[0]
|
2015-05-10 12:35:09 +00:00
|
|
|
|
return pagewidth, pageheight, imgwidthpdf, imgheightpdf
|
2018-12-25 11:30:10 +00:00
|
|
|
|
|
2015-05-10 12:35:09 +00:00
|
|
|
|
return layout_fun
|
|
|
|
|
if border is None:
|
|
|
|
|
border = (0, 0)
|
|
|
|
|
# if the pagesize is given but the imagesize is not, then the imagesize
|
|
|
|
|
# will be calculated from the pagesize, taking into account the border
|
|
|
|
|
# and the fitting
|
|
|
|
|
if pagesize is not None and imgsize is None:
|
2018-12-25 11:30:10 +00:00
|
|
|
|
|
2015-05-10 12:35:09 +00:00
|
|
|
|
def layout_fun(imgwidthpx, imgheightpx, ndpi):
|
2018-12-25 11:30:10 +00:00
|
|
|
|
if (
|
|
|
|
|
pagesize[0] is not None
|
|
|
|
|
and pagesize[1] is not None
|
|
|
|
|
and auto_orient
|
|
|
|
|
and (
|
|
|
|
|
(imgwidthpx > imgheightpx and pagesize[0] < pagesize[1])
|
|
|
|
|
or (imgwidthpx < imgheightpx and pagesize[0] > pagesize[1])
|
|
|
|
|
)
|
|
|
|
|
):
|
2015-05-10 12:35:09 +00:00
|
|
|
|
pagewidth, pageheight = pagesize[1], pagesize[0]
|
|
|
|
|
newborder = border[1], border[0]
|
2014-11-06 07:47:42 +00:00
|
|
|
|
else:
|
2015-05-10 12:35:09 +00:00
|
|
|
|
pagewidth, pageheight = pagesize[0], pagesize[1]
|
|
|
|
|
newborder = border
|
|
|
|
|
if pagewidth is not None:
|
2018-12-25 11:30:10 +00:00
|
|
|
|
fitwidth = pagewidth - 2 * newborder[1]
|
2015-05-10 12:35:09 +00:00
|
|
|
|
else:
|
|
|
|
|
fitwidth = None
|
|
|
|
|
if pageheight is not None:
|
2018-12-25 11:30:10 +00:00
|
|
|
|
fitheight = pageheight - 2 * newborder[0]
|
2015-05-10 12:35:09 +00:00
|
|
|
|
else:
|
|
|
|
|
fitheight = None
|
2018-12-25 11:30:10 +00:00
|
|
|
|
if (
|
|
|
|
|
fit in [FitMode.fill, FitMode.enlarge]
|
|
|
|
|
and fitwidth is not None
|
|
|
|
|
and fitwidth < 0
|
|
|
|
|
and fitheight is not None
|
|
|
|
|
and fitheight < 0
|
|
|
|
|
):
|
2016-02-13 08:31:33 +00:00
|
|
|
|
raise NegativeDimensionError(
|
2015-05-10 12:35:09 +00:00
|
|
|
|
"at least one border dimension musts be smaller than half "
|
2018-12-25 11:30:10 +00:00
|
|
|
|
"the respective page dimension"
|
|
|
|
|
)
|
|
|
|
|
elif fit not in [FitMode.fill, FitMode.enlarge] and (
|
|
|
|
|
(fitwidth is not None and fitwidth < 0)
|
|
|
|
|
or (fitheight is not None and fitheight < 0)
|
|
|
|
|
):
|
2016-02-13 08:31:33 +00:00
|
|
|
|
raise NegativeDimensionError(
|
2015-05-10 12:35:09 +00:00
|
|
|
|
"one border dimension is larger than half of the "
|
2018-12-25 11:30:10 +00:00
|
|
|
|
"respective page dimension"
|
|
|
|
|
)
|
|
|
|
|
imgwidthpdf, imgheightpdf = fitfun(
|
|
|
|
|
fit,
|
|
|
|
|
px_to_pt(imgwidthpx, ndpi[0]),
|
|
|
|
|
px_to_pt(imgheightpx, ndpi[1]),
|
|
|
|
|
fitwidth,
|
|
|
|
|
fitheight,
|
|
|
|
|
)
|
2015-05-10 12:35:09 +00:00
|
|
|
|
if pagewidth is None:
|
2018-12-25 11:30:10 +00:00
|
|
|
|
pagewidth = imgwidthpdf + border[1] * 2
|
2015-05-10 12:35:09 +00:00
|
|
|
|
if pageheight is None:
|
2018-12-25 11:30:10 +00:00
|
|
|
|
pageheight = imgheightpdf + border[0] * 2
|
2015-05-10 12:35:09 +00:00
|
|
|
|
return pagewidth, pageheight, imgwidthpdf, imgheightpdf
|
2018-12-25 11:30:10 +00:00
|
|
|
|
|
2015-05-10 12:35:09 +00:00
|
|
|
|
return layout_fun
|
|
|
|
|
|
|
|
|
|
def scale_imgsize(s, px, dpi):
|
|
|
|
|
if s is None:
|
|
|
|
|
return None
|
|
|
|
|
mode, value = s
|
|
|
|
|
if mode == ImgSize.abs:
|
|
|
|
|
return value
|
|
|
|
|
if mode == ImgSize.perc:
|
2018-12-25 11:30:10 +00:00
|
|
|
|
return (px_to_pt(px, dpi) * value) / 100
|
2015-05-10 12:35:09 +00:00
|
|
|
|
if mode == ImgSize.dpi:
|
|
|
|
|
return px_to_pt(px, value)
|
|
|
|
|
raise NotImplementedError
|
2018-12-25 11:30:10 +00:00
|
|
|
|
|
2015-05-10 12:35:09 +00:00
|
|
|
|
if pagesize is None and imgsize is not None:
|
2018-12-25 11:30:10 +00:00
|
|
|
|
|
2015-05-10 12:35:09 +00:00
|
|
|
|
def layout_fun(imgwidthpx, imgheightpx, ndpi):
|
2018-12-25 11:30:10 +00:00
|
|
|
|
imgwidthpdf, imgheightpdf = fitfun(
|
|
|
|
|
fit,
|
|
|
|
|
px_to_pt(imgwidthpx, ndpi[0]),
|
|
|
|
|
px_to_pt(imgheightpx, ndpi[1]),
|
|
|
|
|
scale_imgsize(imgsize[0], imgwidthpx, ndpi[0]),
|
|
|
|
|
scale_imgsize(imgsize[1], imgheightpx, ndpi[1]),
|
|
|
|
|
)
|
|
|
|
|
pagewidth = imgwidthpdf + 2 * border[1]
|
|
|
|
|
pageheight = imgheightpdf + 2 * border[0]
|
2015-05-10 12:35:09 +00:00
|
|
|
|
return pagewidth, pageheight, imgwidthpdf, imgheightpdf
|
2018-12-25 11:30:10 +00:00
|
|
|
|
|
2015-05-10 12:35:09 +00:00
|
|
|
|
return layout_fun
|
|
|
|
|
if pagesize is not None and imgsize is not None:
|
2018-12-25 11:30:10 +00:00
|
|
|
|
|
2015-05-10 12:35:09 +00:00
|
|
|
|
def layout_fun(imgwidthpx, imgheightpx, ndpi):
|
2018-12-25 11:30:10 +00:00
|
|
|
|
if (
|
|
|
|
|
pagesize[0] is not None
|
|
|
|
|
and pagesize[1] is not None
|
|
|
|
|
and auto_orient
|
|
|
|
|
and (
|
|
|
|
|
(imgwidthpx > imgheightpx and pagesize[0] < pagesize[1])
|
|
|
|
|
or (imgwidthpx < imgheightpx and pagesize[0] > pagesize[1])
|
|
|
|
|
)
|
|
|
|
|
):
|
2015-05-10 12:35:09 +00:00
|
|
|
|
pagewidth, pageheight = pagesize[1], pagesize[0]
|
|
|
|
|
else:
|
|
|
|
|
pagewidth, pageheight = pagesize[0], pagesize[1]
|
2018-12-25 11:30:10 +00:00
|
|
|
|
imgwidthpdf, imgheightpdf = fitfun(
|
|
|
|
|
fit,
|
|
|
|
|
px_to_pt(imgwidthpx, ndpi[0]),
|
|
|
|
|
px_to_pt(imgheightpx, ndpi[1]),
|
|
|
|
|
scale_imgsize(imgsize[0], imgwidthpx, ndpi[0]),
|
|
|
|
|
scale_imgsize(imgsize[1], imgheightpx, ndpi[1]),
|
|
|
|
|
)
|
2015-05-10 12:35:09 +00:00
|
|
|
|
return pagewidth, pageheight, imgwidthpdf, imgheightpdf
|
2018-12-25 11:30:10 +00:00
|
|
|
|
|
2015-05-10 12:35:09 +00:00
|
|
|
|
return layout_fun
|
|
|
|
|
raise NotImplementedError
|
2013-10-23 10:34:07 +00:00
|
|
|
|
|
2014-08-04 15:25:07 +00:00
|
|
|
|
|
2015-05-10 12:35:09 +00:00
|
|
|
|
def default_layout_fun(imgwidthpx, imgheightpx, ndpi):
|
|
|
|
|
imgwidthpdf = pagewidth = px_to_pt(imgwidthpx, ndpi[0])
|
|
|
|
|
imgheightpdf = pageheight = px_to_pt(imgheightpx, ndpi[1])
|
|
|
|
|
return pagewidth, pageheight, imgwidthpdf, imgheightpdf
|
|
|
|
|
|
|
|
|
|
|
2016-02-13 07:59:52 +00:00
|
|
|
|
def get_fixed_dpi_layout_fun(fixed_dpi):
|
|
|
|
|
"""Layout function that overrides whatever DPI is claimed in input images.
|
|
|
|
|
|
|
|
|
|
>>> layout_fun = get_fixed_dpi_layout_fun((300, 300))
|
|
|
|
|
>>> convert(image1, layout_fun=layout_fun, ... outputstream=...)
|
|
|
|
|
"""
|
2018-12-25 11:30:10 +00:00
|
|
|
|
|
2016-02-13 07:59:52 +00:00
|
|
|
|
def fixed_dpi_layout_fun(imgwidthpx, imgheightpx, ndpi):
|
|
|
|
|
return default_layout_fun(imgwidthpx, imgheightpx, fixed_dpi)
|
2018-12-25 11:30:10 +00:00
|
|
|
|
|
2016-02-13 07:59:52 +00:00
|
|
|
|
return fixed_dpi_layout_fun
|
|
|
|
|
|
|
|
|
|
|
2017-07-28 20:52:33 +00:00
|
|
|
|
def find_scale(pagewidth, pageheight):
|
|
|
|
|
"""Find the power of 10 (10, 100, 1000...) that will reduce the scale
|
|
|
|
|
below the PDF specification limit of 14400 PDF units (=200 inches)"""
|
|
|
|
|
from math import log10, ceil
|
|
|
|
|
|
|
|
|
|
major = max(pagewidth, pageheight)
|
|
|
|
|
oversized = major / 14400.0
|
|
|
|
|
|
|
|
|
|
return 10 ** ceil(log10(oversized))
|
|
|
|
|
|
|
|
|
|
|
2016-02-13 07:51:20 +00:00
|
|
|
|
# given one or more input image, depending on outputstream, either return a
|
|
|
|
|
# string containing the whole PDF if outputstream is None or write the PDF
|
|
|
|
|
# data to the given file-like object and return None
|
2016-02-14 17:54:59 +00:00
|
|
|
|
#
|
|
|
|
|
# Input images can be given as file like objects (they must implement read()),
|
|
|
|
|
# as a binary string representing the image content or as filenames to the
|
|
|
|
|
# images.
|
2017-05-22 16:43:27 +00:00
|
|
|
|
def convert(*images, **kwargs):
|
|
|
|
|
|
|
|
|
|
_default_kwargs = dict(
|
2020-05-28 10:46:37 +00:00
|
|
|
|
engine=None,
|
2017-05-22 16:43:27 +00:00
|
|
|
|
title=None,
|
2018-12-25 11:30:10 +00:00
|
|
|
|
author=None,
|
|
|
|
|
creator=None,
|
|
|
|
|
producer=None,
|
|
|
|
|
creationdate=None,
|
|
|
|
|
moddate=None,
|
|
|
|
|
subject=None,
|
|
|
|
|
keywords=None,
|
|
|
|
|
colorspace=None,
|
|
|
|
|
nodate=False,
|
|
|
|
|
layout_fun=default_layout_fun,
|
|
|
|
|
viewer_panes=None,
|
|
|
|
|
viewer_initial_page=None,
|
|
|
|
|
viewer_magnification=None,
|
|
|
|
|
viewer_page_layout=None,
|
|
|
|
|
viewer_fit_window=False,
|
|
|
|
|
viewer_center_window=False,
|
|
|
|
|
viewer_fullscreen=False,
|
|
|
|
|
outputstream=None,
|
|
|
|
|
first_frame_only=False,
|
|
|
|
|
allow_oversized=True,
|
2019-04-02 05:37:03 +00:00
|
|
|
|
cropborder=None,
|
|
|
|
|
bleedborder=None,
|
|
|
|
|
trimborder=None,
|
|
|
|
|
artborder=None,
|
2020-06-01 00:10:07 +00:00
|
|
|
|
pdfa=None,
|
2021-05-14 09:30:23 +00:00
|
|
|
|
rotation=None,
|
2018-12-25 11:30:10 +00:00
|
|
|
|
)
|
2017-05-22 16:43:27 +00:00
|
|
|
|
for kwname, default in _default_kwargs.items():
|
|
|
|
|
if kwname not in kwargs:
|
|
|
|
|
kwargs[kwname] = default
|
|
|
|
|
|
|
|
|
|
pdf = pdfdoc(
|
2020-05-28 10:46:37 +00:00
|
|
|
|
kwargs["engine"],
|
2017-05-22 16:43:27 +00:00
|
|
|
|
"1.3",
|
2018-12-25 11:30:10 +00:00
|
|
|
|
kwargs["title"],
|
|
|
|
|
kwargs["author"],
|
|
|
|
|
kwargs["creator"],
|
|
|
|
|
kwargs["producer"],
|
|
|
|
|
kwargs["creationdate"],
|
|
|
|
|
kwargs["moddate"],
|
|
|
|
|
kwargs["subject"],
|
|
|
|
|
kwargs["keywords"],
|
|
|
|
|
kwargs["nodate"],
|
|
|
|
|
kwargs["viewer_panes"],
|
|
|
|
|
kwargs["viewer_initial_page"],
|
|
|
|
|
kwargs["viewer_magnification"],
|
|
|
|
|
kwargs["viewer_page_layout"],
|
|
|
|
|
kwargs["viewer_fit_window"],
|
|
|
|
|
kwargs["viewer_center_window"],
|
|
|
|
|
kwargs["viewer_fullscreen"],
|
2020-06-01 00:10:07 +00:00
|
|
|
|
kwargs["pdfa"],
|
2018-12-25 11:30:10 +00:00
|
|
|
|
)
|
2015-05-10 12:35:09 +00:00
|
|
|
|
|
2017-01-19 19:32:16 +00:00
|
|
|
|
# backwards compatibility with older img2pdf versions where the first
|
|
|
|
|
# argument to the function had to be given as a list
|
|
|
|
|
if len(images) == 1:
|
|
|
|
|
# if only one argument was given and it is a list, expand it
|
|
|
|
|
if isinstance(images[0], (list, tuple)):
|
|
|
|
|
images = images[0]
|
|
|
|
|
|
2017-05-22 16:43:27 +00:00
|
|
|
|
if not isinstance(images, (list, tuple)):
|
|
|
|
|
images = [images]
|
2020-09-22 13:33:12 +00:00
|
|
|
|
else:
|
|
|
|
|
if len(images) == 0:
|
|
|
|
|
raise ValueError("Unable to process empty list")
|
2017-05-22 16:43:27 +00:00
|
|
|
|
|
2016-02-14 17:54:59 +00:00
|
|
|
|
for img in images:
|
|
|
|
|
# img is allowed to be a path, a binary string representing image data
|
|
|
|
|
# or a file-like object (really anything that implements read())
|
|
|
|
|
try:
|
|
|
|
|
rawdata = img.read()
|
|
|
|
|
except AttributeError:
|
2017-01-19 19:32:46 +00:00
|
|
|
|
if not isinstance(img, (str, bytes)):
|
2018-12-25 11:30:10 +00:00
|
|
|
|
raise TypeError("Neither implements read() nor is str or bytes")
|
2016-02-14 17:54:59 +00:00
|
|
|
|
# the thing doesn't have a read() function, so try if we can treat
|
|
|
|
|
# it as a file name
|
|
|
|
|
try:
|
2020-04-05 10:16:59 +00:00
|
|
|
|
f = open(img, "rb")
|
2018-08-01 20:28:44 +00:00
|
|
|
|
except Exception:
|
2016-02-14 17:54:59 +00:00
|
|
|
|
# whatever the exception is (string could contain NUL
|
|
|
|
|
# characters or the path could just not exist) it's not a file
|
|
|
|
|
# name so we now try treating it as raw image content
|
|
|
|
|
rawdata = img
|
2020-04-05 10:16:59 +00:00
|
|
|
|
else:
|
|
|
|
|
# we are not using a "with" block here because we only want to
|
|
|
|
|
# catch exceptions thrown by open(). The read() may throw its
|
|
|
|
|
# own exceptions like MemoryError which should be handled
|
|
|
|
|
# differently.
|
|
|
|
|
rawdata = f.read()
|
|
|
|
|
f.close()
|
2016-02-14 17:54:59 +00:00
|
|
|
|
|
2018-12-25 11:30:10 +00:00
|
|
|
|
for (
|
|
|
|
|
color,
|
|
|
|
|
ndpi,
|
|
|
|
|
imgformat,
|
|
|
|
|
imgdata,
|
2021-06-12 20:03:56 +00:00
|
|
|
|
smaskdata,
|
2018-12-25 11:30:10 +00:00
|
|
|
|
imgwidthpx,
|
|
|
|
|
imgheightpx,
|
|
|
|
|
palette,
|
|
|
|
|
inverted,
|
|
|
|
|
depth,
|
|
|
|
|
rotation,
|
2020-08-06 22:13:53 +00:00
|
|
|
|
iccp,
|
2021-05-14 09:30:23 +00:00
|
|
|
|
) in read_images(
|
|
|
|
|
rawdata,
|
|
|
|
|
kwargs["colorspace"],
|
|
|
|
|
kwargs["first_frame_only"],
|
|
|
|
|
kwargs["rotation"],
|
|
|
|
|
):
|
2018-12-25 11:30:10 +00:00
|
|
|
|
pagewidth, pageheight, imgwidthpdf, imgheightpdf = kwargs["layout_fun"](
|
|
|
|
|
imgwidthpx, imgheightpx, ndpi
|
|
|
|
|
)
|
2017-07-28 20:52:33 +00:00
|
|
|
|
|
|
|
|
|
userunit = None
|
2016-02-16 23:30:15 +00:00
|
|
|
|
if pagewidth < 3.00 or pageheight < 3.00:
|
2021-04-12 12:12:14 +00:00
|
|
|
|
logger.warning(
|
2019-03-28 23:34:31 +00:00
|
|
|
|
"pdf width or height is below 3.00 - too small for some viewers!"
|
2018-12-25 11:30:10 +00:00
|
|
|
|
)
|
2016-02-16 23:30:15 +00:00
|
|
|
|
elif pagewidth > 14400.0 or pageheight > 14400.0:
|
2018-12-25 11:30:10 +00:00
|
|
|
|
if kwargs["allow_oversized"]:
|
2017-07-28 20:52:33 +00:00
|
|
|
|
userunit = find_scale(pagewidth, pageheight)
|
|
|
|
|
pagewidth /= userunit
|
|
|
|
|
pageheight /= userunit
|
|
|
|
|
imgwidthpdf /= userunit
|
|
|
|
|
imgheightpdf /= userunit
|
|
|
|
|
else:
|
|
|
|
|
raise PdfTooLargeError(
|
2018-12-25 11:30:10 +00:00
|
|
|
|
"pdf width or height must not exceed 200 inches."
|
|
|
|
|
)
|
2021-04-13 11:10:57 +00:00
|
|
|
|
for border in ["crop", "bleed", "trim", "art"]:
|
|
|
|
|
if kwargs[border + "border"] is None:
|
|
|
|
|
continue
|
|
|
|
|
if pagewidth < 2 * kwargs[border + "border"][1]:
|
|
|
|
|
raise ValueError(
|
|
|
|
|
"horizontal %s border larger than page width" % border
|
|
|
|
|
)
|
|
|
|
|
if pageheight < 2 * kwargs[border + "border"][0]:
|
|
|
|
|
raise ValueError(
|
|
|
|
|
"vertical %s border larger than page height" % border
|
|
|
|
|
)
|
2016-02-16 23:30:15 +00:00
|
|
|
|
# the image is always centered on the page
|
2018-12-25 11:30:10 +00:00
|
|
|
|
imgxpdf = (pagewidth - imgwidthpdf) / 2.0
|
|
|
|
|
imgypdf = (pageheight - imgheightpdf) / 2.0
|
|
|
|
|
pdf.add_imagepage(
|
|
|
|
|
color,
|
|
|
|
|
imgwidthpx,
|
|
|
|
|
imgheightpx,
|
|
|
|
|
imgformat,
|
|
|
|
|
imgdata,
|
2021-06-12 20:03:56 +00:00
|
|
|
|
smaskdata,
|
2018-12-25 11:30:10 +00:00
|
|
|
|
imgwidthpdf,
|
|
|
|
|
imgheightpdf,
|
|
|
|
|
imgxpdf,
|
|
|
|
|
imgypdf,
|
|
|
|
|
pagewidth,
|
|
|
|
|
pageheight,
|
|
|
|
|
userunit,
|
|
|
|
|
palette,
|
|
|
|
|
inverted,
|
|
|
|
|
depth,
|
|
|
|
|
rotation,
|
2019-04-02 05:37:03 +00:00
|
|
|
|
kwargs["cropborder"],
|
|
|
|
|
kwargs["bleedborder"],
|
|
|
|
|
kwargs["trimborder"],
|
|
|
|
|
kwargs["artborder"],
|
2020-08-06 22:13:53 +00:00
|
|
|
|
iccp,
|
2018-12-25 11:30:10 +00:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if kwargs["outputstream"]:
|
|
|
|
|
pdf.tostream(kwargs["outputstream"])
|
2016-02-13 07:51:20 +00:00
|
|
|
|
return
|
|
|
|
|
|
2013-10-23 10:34:07 +00:00
|
|
|
|
return pdf.tostring()
|
2012-03-29 09:08:32 +00:00
|
|
|
|
|
2015-03-20 10:37:30 +00:00
|
|
|
|
|
2015-05-10 12:35:09 +00:00
|
|
|
|
def parse_num(num, name):
|
2018-12-25 11:30:10 +00:00
|
|
|
|
if num == "":
|
2015-05-10 12:35:09 +00:00
|
|
|
|
return None
|
|
|
|
|
unit = None
|
|
|
|
|
if num.endswith("pt"):
|
|
|
|
|
unit = Unit.pt
|
|
|
|
|
elif num.endswith("cm"):
|
|
|
|
|
unit = Unit.cm
|
|
|
|
|
elif num.endswith("mm"):
|
|
|
|
|
unit = Unit.mm
|
|
|
|
|
elif num.endswith("in"):
|
|
|
|
|
unit = Unit.inch
|
|
|
|
|
else:
|
|
|
|
|
try:
|
|
|
|
|
num = float(num)
|
|
|
|
|
except ValueError:
|
2018-12-25 11:30:10 +00:00
|
|
|
|
msg = (
|
|
|
|
|
"%s is not a floating point number and doesn't have a "
|
|
|
|
|
"valid unit: %s" % (name, num)
|
|
|
|
|
)
|
2015-05-10 12:35:09 +00:00
|
|
|
|
raise argparse.ArgumentTypeError(msg)
|
|
|
|
|
if unit is None:
|
|
|
|
|
unit = Unit.pt
|
|
|
|
|
else:
|
|
|
|
|
num = num[:-2]
|
|
|
|
|
try:
|
|
|
|
|
num = float(num)
|
|
|
|
|
except ValueError:
|
|
|
|
|
msg = "%s is not a floating point number: %s" % (name, num)
|
|
|
|
|
raise argparse.ArgumentTypeError(msg)
|
2020-08-03 10:25:14 +00:00
|
|
|
|
if num < 0:
|
|
|
|
|
msg = "%s must not be negative: %s" % (name, num)
|
|
|
|
|
raise argparse.ArgumentTypeError(msg)
|
2015-05-10 12:35:09 +00:00
|
|
|
|
if unit == Unit.cm:
|
|
|
|
|
num = cm_to_pt(num)
|
|
|
|
|
elif unit == Unit.mm:
|
|
|
|
|
num = mm_to_pt(num)
|
|
|
|
|
elif unit == Unit.inch:
|
|
|
|
|
num = in_to_pt(num)
|
|
|
|
|
return num
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def parse_imgsize_num(num, name):
|
2018-12-25 11:30:10 +00:00
|
|
|
|
if num == "":
|
2015-05-10 12:35:09 +00:00
|
|
|
|
return None
|
|
|
|
|
unit = None
|
|
|
|
|
if num.endswith("pt"):
|
|
|
|
|
unit = ImgUnit.pt
|
|
|
|
|
elif num.endswith("cm"):
|
|
|
|
|
unit = ImgUnit.cm
|
|
|
|
|
elif num.endswith("mm"):
|
|
|
|
|
unit = ImgUnit.mm
|
|
|
|
|
elif num.endswith("in"):
|
|
|
|
|
unit = ImgUnit.inch
|
|
|
|
|
elif num.endswith("dpi"):
|
|
|
|
|
unit = ImgUnit.dpi
|
|
|
|
|
elif num.endswith("%"):
|
|
|
|
|
unit = ImgUnit.perc
|
|
|
|
|
else:
|
|
|
|
|
try:
|
|
|
|
|
num = float(num)
|
|
|
|
|
except ValueError:
|
2018-12-25 11:30:10 +00:00
|
|
|
|
msg = (
|
|
|
|
|
"%s is not a floating point number and doesn't have a "
|
|
|
|
|
"valid unit: %s" % (name, num)
|
|
|
|
|
)
|
2015-05-10 12:35:09 +00:00
|
|
|
|
raise argparse.ArgumentTypeError(msg)
|
|
|
|
|
if unit is None:
|
|
|
|
|
unit = ImgUnit.pt
|
|
|
|
|
else:
|
2016-02-10 08:32:38 +00:00
|
|
|
|
# strip off unit from string
|
|
|
|
|
if unit == ImgUnit.dpi:
|
|
|
|
|
num = num[:-3]
|
|
|
|
|
elif unit == ImgUnit.perc:
|
|
|
|
|
num = num[:-1]
|
|
|
|
|
else:
|
|
|
|
|
num = num[:-2]
|
2015-05-10 12:35:09 +00:00
|
|
|
|
try:
|
|
|
|
|
num = float(num)
|
|
|
|
|
except ValueError:
|
|
|
|
|
msg = "%s is not a floating point number: %s" % (name, num)
|
|
|
|
|
raise argparse.ArgumentTypeError(msg)
|
|
|
|
|
if unit == ImgUnit.cm:
|
|
|
|
|
num = (ImgSize.abs, cm_to_pt(num))
|
|
|
|
|
elif unit == ImgUnit.mm:
|
|
|
|
|
num = (ImgSize.abs, mm_to_pt(num))
|
|
|
|
|
elif unit == ImgUnit.inch:
|
|
|
|
|
num = (ImgSize.abs, in_to_pt(num))
|
|
|
|
|
elif unit == ImgUnit.pt:
|
|
|
|
|
num = (ImgSize.abs, num)
|
|
|
|
|
elif unit == ImgUnit.dpi:
|
|
|
|
|
num = (ImgSize.dpi, num)
|
|
|
|
|
elif unit == ImgUnit.perc:
|
|
|
|
|
num = (ImgSize.perc, num)
|
|
|
|
|
return num
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def parse_pagesize_rectarg(string):
|
|
|
|
|
transposed = string.endswith("^T")
|
|
|
|
|
if transposed:
|
|
|
|
|
string = string[:-2]
|
|
|
|
|
if papersizes.get(string.lower()):
|
|
|
|
|
string = papersizes[string.lower()]
|
2018-12-25 11:30:10 +00:00
|
|
|
|
if "x" not in string:
|
2016-02-17 17:16:17 +00:00
|
|
|
|
# if there is no separating "x" in the string, then the string is
|
|
|
|
|
# interpreted as the width
|
|
|
|
|
w = parse_num(string, "width")
|
|
|
|
|
h = None
|
|
|
|
|
else:
|
2018-12-25 11:30:10 +00:00
|
|
|
|
w, h = string.split("x", 1)
|
2016-02-17 17:16:17 +00:00
|
|
|
|
w = parse_num(w, "width")
|
|
|
|
|
h = parse_num(h, "height")
|
2015-05-10 12:35:09 +00:00
|
|
|
|
if transposed:
|
|
|
|
|
w, h = h, w
|
|
|
|
|
if w is None and h is None:
|
2019-03-28 23:34:31 +00:00
|
|
|
|
raise argparse.ArgumentTypeError("at least one dimension must be specified")
|
2015-05-10 12:35:09 +00:00
|
|
|
|
return w, h
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def parse_imgsize_rectarg(string):
|
|
|
|
|
transposed = string.endswith("^T")
|
|
|
|
|
if transposed:
|
|
|
|
|
string = string[:-2]
|
|
|
|
|
if papersizes.get(string.lower()):
|
|
|
|
|
string = papersizes[string.lower()]
|
2018-12-25 11:30:10 +00:00
|
|
|
|
if "x" not in string:
|
2016-02-17 17:16:17 +00:00
|
|
|
|
# if there is no separating "x" in the string, then the string is
|
|
|
|
|
# interpreted as the width
|
|
|
|
|
w = parse_imgsize_num(string, "width")
|
|
|
|
|
h = None
|
|
|
|
|
else:
|
2018-12-25 11:30:10 +00:00
|
|
|
|
w, h = string.split("x", 1)
|
2016-02-17 17:16:17 +00:00
|
|
|
|
w = parse_imgsize_num(w, "width")
|
|
|
|
|
h = parse_imgsize_num(h, "height")
|
2015-05-10 12:35:09 +00:00
|
|
|
|
if transposed:
|
|
|
|
|
w, h = h, w
|
|
|
|
|
if w is None and h is None:
|
2019-03-28 23:34:31 +00:00
|
|
|
|
raise argparse.ArgumentTypeError("at least one dimension must be specified")
|
2015-05-10 12:35:09 +00:00
|
|
|
|
return w, h
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def parse_colorspacearg(string):
|
|
|
|
|
for c in Colorspace:
|
|
|
|
|
if c.name == string:
|
|
|
|
|
return c
|
|
|
|
|
allowed = ", ".join([c.name for c in Colorspace])
|
2018-12-25 11:30:10 +00:00
|
|
|
|
raise argparse.ArgumentTypeError(
|
2019-03-28 23:34:31 +00:00
|
|
|
|
"Unsupported colorspace: %s. Must be one of: %s." % (string, allowed)
|
2018-12-25 11:30:10 +00:00
|
|
|
|
)
|
2015-05-10 12:35:09 +00:00
|
|
|
|
|
|
|
|
|
|
2020-05-28 10:46:37 +00:00
|
|
|
|
def parse_enginearg(string):
|
|
|
|
|
for c in Engine:
|
|
|
|
|
if c.name == string:
|
|
|
|
|
return c
|
|
|
|
|
allowed = ", ".join([c.name for c in Engine])
|
|
|
|
|
raise argparse.ArgumentTypeError(
|
|
|
|
|
"Unsupported engine: %s. Must be one of: %s." % (string, allowed)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2015-05-10 12:35:09 +00:00
|
|
|
|
def parse_borderarg(string):
|
2018-12-25 11:30:10 +00:00
|
|
|
|
if ":" in string:
|
|
|
|
|
h, v = string.split(":", 1)
|
|
|
|
|
if h == "":
|
2015-05-10 12:35:09 +00:00
|
|
|
|
raise argparse.ArgumentTypeError("missing value before colon")
|
2018-12-25 11:30:10 +00:00
|
|
|
|
if v == "":
|
2015-05-10 12:35:09 +00:00
|
|
|
|
raise argparse.ArgumentTypeError("missing value after colon")
|
|
|
|
|
else:
|
2018-12-25 11:30:10 +00:00
|
|
|
|
if string == "":
|
2015-05-10 12:35:09 +00:00
|
|
|
|
raise argparse.ArgumentTypeError("border option cannot be empty")
|
|
|
|
|
h, v = string, string
|
|
|
|
|
h, v = parse_num(h, "left/right border"), parse_num(v, "top/bottom border")
|
|
|
|
|
if h is None and v is None:
|
|
|
|
|
raise argparse.ArgumentTypeError("missing value")
|
|
|
|
|
return h, v
|
|
|
|
|
|
|
|
|
|
|
2021-05-07 08:58:46 +00:00
|
|
|
|
def from_file(path):
|
|
|
|
|
result = []
|
|
|
|
|
if path == "-":
|
|
|
|
|
content = sys.stdin.buffer.read()
|
|
|
|
|
else:
|
|
|
|
|
with open(path, "rb") as f:
|
|
|
|
|
content = f.read()
|
|
|
|
|
for path in content.split(b"\0"):
|
|
|
|
|
if path == b"":
|
|
|
|
|
continue
|
|
|
|
|
try:
|
|
|
|
|
# test-read a byte from it so that we can abort early in case
|
|
|
|
|
# we cannot read data from the file
|
|
|
|
|
with open(path, "rb") as im:
|
|
|
|
|
im.read(1)
|
|
|
|
|
except IsADirectoryError:
|
|
|
|
|
raise argparse.ArgumentTypeError('"%s" is a directory' % path)
|
|
|
|
|
except PermissionError:
|
|
|
|
|
raise argparse.ArgumentTypeError('"%s" permission denied' % path)
|
|
|
|
|
except FileNotFoundError:
|
|
|
|
|
raise argparse.ArgumentTypeError('"%s" does not exist' % path)
|
|
|
|
|
result.append(path)
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
2020-09-23 16:39:06 +00:00
|
|
|
|
def input_images(path_expr):
|
|
|
|
|
if path_expr == "-":
|
2016-02-14 17:54:59 +00:00
|
|
|
|
# we slurp in all data from stdin because we need to seek in it later
|
2021-04-13 06:12:33 +00:00
|
|
|
|
result = [sys.stdin.buffer.read()]
|
2016-02-14 17:54:59 +00:00
|
|
|
|
if len(result) == 0:
|
2020-09-23 16:39:06 +00:00
|
|
|
|
raise argparse.ArgumentTypeError('"%s" is empty' % path_expr)
|
2015-03-20 10:37:30 +00:00
|
|
|
|
else:
|
2020-09-23 16:39:06 +00:00
|
|
|
|
result = []
|
|
|
|
|
paths = [path_expr]
|
|
|
|
|
if sys.platform == "win32" and ("*" in path_expr or "?" in path_expr):
|
|
|
|
|
# on windows, program is responsible for expanding wildcards such as *.jpg
|
|
|
|
|
# glob won't return files that don't exist so we only use it for wildcards
|
|
|
|
|
# paths without wildcards that do not exist will trigger "does not exist"
|
|
|
|
|
from glob import glob
|
2021-04-12 12:13:34 +00:00
|
|
|
|
|
2020-09-23 16:39:06 +00:00
|
|
|
|
paths = glob(path_expr)
|
|
|
|
|
for path in paths:
|
|
|
|
|
try:
|
|
|
|
|
if os.path.getsize(path) == 0:
|
|
|
|
|
raise argparse.ArgumentTypeError('"%s" is empty' % path)
|
|
|
|
|
# test-read a byte from it so that we can abort early in case
|
|
|
|
|
# we cannot read data from the file
|
|
|
|
|
with open(path, "rb") as im:
|
|
|
|
|
im.read(1)
|
|
|
|
|
except IsADirectoryError:
|
|
|
|
|
raise argparse.ArgumentTypeError('"%s" is a directory' % path)
|
|
|
|
|
except PermissionError:
|
|
|
|
|
raise argparse.ArgumentTypeError('"%s" permission denied' % path)
|
|
|
|
|
except FileNotFoundError:
|
|
|
|
|
raise argparse.ArgumentTypeError('"%s" does not exist' % path)
|
|
|
|
|
result.append(path)
|
2016-02-14 17:54:59 +00:00
|
|
|
|
return result
|
2015-05-10 12:35:09 +00:00
|
|
|
|
|
|
|
|
|
|
2021-05-14 09:30:23 +00:00
|
|
|
|
def parse_rotationarg(string):
|
|
|
|
|
for m in Rotation:
|
|
|
|
|
if m.name == string.lower():
|
|
|
|
|
return m
|
|
|
|
|
raise argparse.ArgumentTypeError("unknown rotation value: %s" % string)
|
|
|
|
|
|
|
|
|
|
|
2015-05-10 12:35:09 +00:00
|
|
|
|
def parse_fitarg(string):
|
|
|
|
|
for m in FitMode:
|
|
|
|
|
if m.name == string.lower():
|
|
|
|
|
return m
|
|
|
|
|
raise argparse.ArgumentTypeError("unknown fit mode: %s" % string)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def parse_panes(string):
|
|
|
|
|
for m in PageMode:
|
|
|
|
|
if m.name == string.lower():
|
|
|
|
|
return m
|
|
|
|
|
allowed = ", ".join([m.name for m in PageMode])
|
2018-12-25 11:30:10 +00:00
|
|
|
|
raise argparse.ArgumentTypeError(
|
2019-03-28 23:34:31 +00:00
|
|
|
|
"Unsupported page mode: %s. Must be one of: %s." % (string, allowed)
|
2018-12-25 11:30:10 +00:00
|
|
|
|
)
|
2015-05-10 12:35:09 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def parse_magnification(string):
|
|
|
|
|
for m in Magnification:
|
|
|
|
|
if m.name == string.lower():
|
|
|
|
|
return m
|
|
|
|
|
try:
|
|
|
|
|
return float(string)
|
|
|
|
|
except ValueError:
|
|
|
|
|
pass
|
|
|
|
|
allowed = ", ".join([m.name for m in Magnification])
|
2018-12-25 11:30:10 +00:00
|
|
|
|
raise argparse.ArgumentTypeError(
|
|
|
|
|
"Unsupported magnification: %s. Must be "
|
|
|
|
|
"a floating point number or one of: %s." % (string, allowed)
|
|
|
|
|
)
|
2015-05-10 12:35:09 +00:00
|
|
|
|
|
2015-03-20 10:37:30 +00:00
|
|
|
|
|
2015-05-10 12:35:09 +00:00
|
|
|
|
def parse_layout(string):
|
|
|
|
|
for l in PageLayout:
|
|
|
|
|
if l.name == string.lower():
|
|
|
|
|
return l
|
|
|
|
|
allowed = ", ".join([l.name for l in PageLayout])
|
2018-12-25 11:30:10 +00:00
|
|
|
|
raise argparse.ArgumentTypeError(
|
2019-03-28 23:34:31 +00:00
|
|
|
|
"Unsupported page layout: %s. Must be one of: %s." % (string, allowed)
|
2018-12-25 11:30:10 +00:00
|
|
|
|
)
|
2014-03-01 03:57:40 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def valid_date(string):
|
2015-03-13 13:29:53 +00:00
|
|
|
|
# first try parsing in ISO8601 format
|
|
|
|
|
try:
|
|
|
|
|
return datetime.strptime(string, "%Y-%m-%d")
|
|
|
|
|
except ValueError:
|
|
|
|
|
pass
|
|
|
|
|
try:
|
|
|
|
|
return datetime.strptime(string, "%Y-%m-%dT%H:%M")
|
|
|
|
|
except ValueError:
|
|
|
|
|
pass
|
|
|
|
|
try:
|
|
|
|
|
return datetime.strptime(string, "%Y-%m-%dT%H:%M:%S")
|
|
|
|
|
except ValueError:
|
|
|
|
|
pass
|
|
|
|
|
# then try dateutil
|
|
|
|
|
try:
|
|
|
|
|
from dateutil import parser
|
|
|
|
|
except ImportError:
|
|
|
|
|
pass
|
|
|
|
|
else:
|
|
|
|
|
try:
|
|
|
|
|
return parser.parse(string)
|
|
|
|
|
except TypeError:
|
|
|
|
|
pass
|
|
|
|
|
# as a last resort, try the local date utility
|
|
|
|
|
try:
|
|
|
|
|
import subprocess
|
|
|
|
|
except ImportError:
|
|
|
|
|
pass
|
|
|
|
|
else:
|
|
|
|
|
try:
|
|
|
|
|
utime = subprocess.check_output(["date", "--date", string, "+%s"])
|
|
|
|
|
except subprocess.CalledProcessError:
|
|
|
|
|
pass
|
|
|
|
|
else:
|
|
|
|
|
return datetime.utcfromtimestamp(int(utime))
|
2015-05-10 12:35:09 +00:00
|
|
|
|
raise argparse.ArgumentTypeError("cannot parse date: %s" % string)
|
|
|
|
|
|
|
|
|
|
|
2019-08-25 04:03:48 +00:00
|
|
|
|
def gui():
|
|
|
|
|
import tkinter
|
|
|
|
|
import tkinter.filedialog
|
|
|
|
|
|
2020-04-05 10:21:09 +00:00
|
|
|
|
have_fitz = True
|
|
|
|
|
try:
|
|
|
|
|
import fitz
|
|
|
|
|
except ImportError:
|
|
|
|
|
have_fitz = False
|
|
|
|
|
|
2019-08-25 04:03:48 +00:00
|
|
|
|
# from Python 3.7 Lib/idlelib/configdialog.py
|
|
|
|
|
# Copyright 2015-2017 Terry Jan Reedy
|
|
|
|
|
# Python License
|
|
|
|
|
class VerticalScrolledFrame(tkinter.Frame):
|
|
|
|
|
"""A pure Tkinter vertically scrollable frame.
|
|
|
|
|
|
|
|
|
|
* Use the 'interior' attribute to place widgets inside the scrollable frame
|
|
|
|
|
* Construct and pack/place/grid normally
|
|
|
|
|
* This frame only allows vertical scrolling
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def __init__(self, parent, *args, **kw):
|
|
|
|
|
tkinter.Frame.__init__(self, parent, *args, **kw)
|
|
|
|
|
|
|
|
|
|
# Create a canvas object and a vertical scrollbar for scrolling it.
|
|
|
|
|
vscrollbar = tkinter.Scrollbar(self, orient=tkinter.VERTICAL)
|
|
|
|
|
vscrollbar.pack(fill=tkinter.Y, side=tkinter.RIGHT, expand=tkinter.FALSE)
|
|
|
|
|
canvas = tkinter.Canvas(
|
|
|
|
|
self,
|
|
|
|
|
borderwidth=0,
|
|
|
|
|
highlightthickness=0,
|
|
|
|
|
yscrollcommand=vscrollbar.set,
|
|
|
|
|
width=240,
|
|
|
|
|
)
|
|
|
|
|
canvas.pack(side=tkinter.LEFT, fill=tkinter.BOTH, expand=tkinter.TRUE)
|
|
|
|
|
vscrollbar.config(command=canvas.yview)
|
|
|
|
|
|
|
|
|
|
# Reset the view.
|
|
|
|
|
canvas.xview_moveto(0)
|
|
|
|
|
canvas.yview_moveto(0)
|
|
|
|
|
|
|
|
|
|
# Create a frame inside the canvas which will be scrolled with it.
|
|
|
|
|
self.interior = interior = tkinter.Frame(canvas)
|
|
|
|
|
interior_id = canvas.create_window(0, 0, window=interior, anchor=tkinter.NW)
|
|
|
|
|
|
|
|
|
|
# Track changes to the canvas and frame width and sync them,
|
|
|
|
|
# also updating the scrollbar.
|
|
|
|
|
def _configure_interior(event):
|
|
|
|
|
# Update the scrollbars to match the size of the inner frame.
|
|
|
|
|
size = (interior.winfo_reqwidth(), interior.winfo_reqheight())
|
|
|
|
|
canvas.config(scrollregion="0 0 %s %s" % size)
|
|
|
|
|
|
|
|
|
|
interior.bind("<Configure>", _configure_interior)
|
|
|
|
|
|
|
|
|
|
def _configure_canvas(event):
|
|
|
|
|
if interior.winfo_reqwidth() != canvas.winfo_width():
|
|
|
|
|
# Update the inner frame's width to fill the canvas.
|
|
|
|
|
canvas.itemconfigure(interior_id, width=canvas.winfo_width())
|
|
|
|
|
|
|
|
|
|
canvas.bind("<Configure>", _configure_canvas)
|
|
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# From Python 3.7 Lib/tkinter/__init__.py
|
|
|
|
|
# Copyright 2000 Fredrik Lundh
|
|
|
|
|
# Python License
|
|
|
|
|
#
|
|
|
|
|
# add support for 'state' and 'name' kwargs
|
|
|
|
|
# add support for updating list of options
|
|
|
|
|
class OptionMenu(tkinter.Menubutton):
|
|
|
|
|
"""OptionMenu which allows the user to select a value from a menu."""
|
|
|
|
|
|
|
|
|
|
def __init__(self, master, variable, value, *values, **kwargs):
|
|
|
|
|
"""Construct an optionmenu widget with the parent MASTER, with
|
|
|
|
|
the resource textvariable set to VARIABLE, the initially selected
|
|
|
|
|
value VALUE, the other menu values VALUES and an additional
|
|
|
|
|
keyword argument command."""
|
|
|
|
|
kw = {
|
|
|
|
|
"borderwidth": 2,
|
|
|
|
|
"textvariable": variable,
|
|
|
|
|
"indicatoron": 1,
|
|
|
|
|
"relief": tkinter.RAISED,
|
|
|
|
|
"anchor": "c",
|
|
|
|
|
"highlightthickness": 2,
|
|
|
|
|
}
|
|
|
|
|
if "state" in kwargs:
|
|
|
|
|
kw["state"] = kwargs["state"]
|
|
|
|
|
del kwargs["state"]
|
|
|
|
|
if "name" in kwargs:
|
|
|
|
|
kw["name"] = kwargs["name"]
|
|
|
|
|
del kwargs["name"]
|
|
|
|
|
tkinter.Widget.__init__(self, master, "menubutton", kw)
|
|
|
|
|
self.widgetName = "tk_optionMenu"
|
|
|
|
|
self.callback = kwargs.get("command")
|
|
|
|
|
self.variable = variable
|
|
|
|
|
if "command" in kwargs:
|
|
|
|
|
del kwargs["command"]
|
|
|
|
|
if kwargs:
|
|
|
|
|
raise tkinter.TclError("unknown option -" + list(kwargs.keys())[0])
|
|
|
|
|
self.set_values([value] + list(values))
|
|
|
|
|
|
|
|
|
|
def __getitem__(self, name):
|
|
|
|
|
if name == "menu":
|
|
|
|
|
return self.__menu
|
|
|
|
|
return tkinter.Widget.__getitem__(self, name)
|
|
|
|
|
|
|
|
|
|
def set_values(self, values):
|
|
|
|
|
menu = self.__menu = tkinter.Menu(self, name="menu", tearoff=0)
|
|
|
|
|
self.menuname = menu._w
|
|
|
|
|
for v in values:
|
|
|
|
|
menu.add_command(
|
|
|
|
|
label=v, command=tkinter._setit(self.variable, v, self.callback)
|
|
|
|
|
)
|
|
|
|
|
self["menu"] = menu
|
|
|
|
|
|
|
|
|
|
def destroy(self):
|
|
|
|
|
"""Destroy this widget and the associated menu."""
|
|
|
|
|
tkinter.Menubutton.destroy(self)
|
|
|
|
|
self.__menu = None
|
|
|
|
|
|
|
|
|
|
root = tkinter.Tk()
|
|
|
|
|
app = tkinter.Frame(master=root)
|
|
|
|
|
|
2020-04-05 10:21:09 +00:00
|
|
|
|
infiles = []
|
|
|
|
|
maxpagewidth = 0
|
|
|
|
|
maxpageheight = 0
|
|
|
|
|
doc = None
|
|
|
|
|
|
|
|
|
|
args = {
|
2020-05-28 10:46:37 +00:00
|
|
|
|
"engine": tkinter.StringVar(),
|
2020-04-05 10:21:09 +00:00
|
|
|
|
"first_frame_only": tkinter.BooleanVar(),
|
|
|
|
|
"auto_orient": tkinter.BooleanVar(),
|
|
|
|
|
"fit": tkinter.StringVar(),
|
|
|
|
|
"title": tkinter.StringVar(),
|
|
|
|
|
"author": tkinter.StringVar(),
|
|
|
|
|
"creator": tkinter.StringVar(),
|
|
|
|
|
"producer": tkinter.StringVar(),
|
|
|
|
|
"subject": tkinter.StringVar(),
|
|
|
|
|
"keywords": tkinter.StringVar(),
|
|
|
|
|
"nodate": tkinter.BooleanVar(),
|
|
|
|
|
"creationdate": tkinter.StringVar(),
|
|
|
|
|
"moddate": tkinter.StringVar(),
|
|
|
|
|
"viewer_panes": tkinter.StringVar(),
|
|
|
|
|
"viewer_initial_page": tkinter.IntVar(),
|
|
|
|
|
"viewer_magnification": tkinter.StringVar(),
|
|
|
|
|
"viewer_page_layout": tkinter.StringVar(),
|
|
|
|
|
"viewer_fit_window": tkinter.BooleanVar(),
|
|
|
|
|
"viewer_center_window": tkinter.BooleanVar(),
|
|
|
|
|
"viewer_fullscreen": tkinter.BooleanVar(),
|
|
|
|
|
"pagesize_dropdown": tkinter.StringVar(),
|
|
|
|
|
"pagesize_width": tkinter.DoubleVar(),
|
|
|
|
|
"pagesize_height": tkinter.DoubleVar(),
|
|
|
|
|
"imgsize_dropdown": tkinter.StringVar(),
|
|
|
|
|
"imgsize_width": tkinter.DoubleVar(),
|
|
|
|
|
"imgsize_height": tkinter.DoubleVar(),
|
|
|
|
|
"colorspace": tkinter.StringVar(),
|
|
|
|
|
"first_frame_only": tkinter.BooleanVar(),
|
|
|
|
|
}
|
2020-05-28 10:46:37 +00:00
|
|
|
|
args["engine"].set("auto")
|
2020-04-05 10:21:09 +00:00
|
|
|
|
args["title"].set("")
|
|
|
|
|
args["auto_orient"].set(False)
|
|
|
|
|
args["fit"].set("into")
|
|
|
|
|
args["colorspace"].set("auto")
|
|
|
|
|
args["viewer_panes"].set("auto")
|
|
|
|
|
args["viewer_initial_page"].set(1)
|
|
|
|
|
args["viewer_magnification"].set("auto")
|
|
|
|
|
args["viewer_page_layout"].set("auto")
|
|
|
|
|
args["first_frame_only"].set(False)
|
|
|
|
|
args["pagesize_dropdown"].set("auto")
|
|
|
|
|
args["imgsize_dropdown"].set("auto")
|
2019-08-25 04:03:48 +00:00
|
|
|
|
|
|
|
|
|
def on_open_button():
|
2020-04-05 10:21:09 +00:00
|
|
|
|
nonlocal infiles
|
|
|
|
|
nonlocal doc
|
|
|
|
|
nonlocal maxpagewidth
|
|
|
|
|
nonlocal maxpageheight
|
|
|
|
|
infiles = tkinter.filedialog.askopenfilenames(
|
|
|
|
|
parent=root,
|
|
|
|
|
title="open image",
|
|
|
|
|
filetypes=[
|
|
|
|
|
(
|
|
|
|
|
"images",
|
|
|
|
|
"*.bmp *.eps *.gif *.ico *.jpeg *.jpg *.jp2 *.pcx *.png *.ppm *.tiff",
|
|
|
|
|
),
|
|
|
|
|
("all files", "*"),
|
|
|
|
|
],
|
|
|
|
|
# initialdir="/home/josch/git/plakativ",
|
|
|
|
|
# initialfile="test.pdf",
|
|
|
|
|
)
|
|
|
|
|
if have_fitz:
|
|
|
|
|
with BytesIO() as f:
|
|
|
|
|
save_pdf(f)
|
|
|
|
|
f.seek(0)
|
|
|
|
|
doc = fitz.open(stream=f, filetype="pdf")
|
|
|
|
|
for page in doc:
|
|
|
|
|
if page.getDisplayList().rect.width > maxpagewidth:
|
|
|
|
|
maxpagewidth = page.getDisplayList().rect.width
|
|
|
|
|
if page.getDisplayList().rect.height > maxpageheight:
|
|
|
|
|
maxpageheight = page.getDisplayList().rect.height
|
|
|
|
|
draw()
|
|
|
|
|
|
|
|
|
|
def save_pdf(stream):
|
|
|
|
|
pagesizearg = None
|
|
|
|
|
if args["pagesize_dropdown"].get() == "auto":
|
|
|
|
|
# nothing to do
|
|
|
|
|
pass
|
|
|
|
|
elif args["pagesize_dropdown"].get() == "custom":
|
|
|
|
|
pagesizearg = args["pagesize_width"].get(), args["pagesize_height"].get()
|
|
|
|
|
elif args["pagesize_dropdown"].get() in papernames.values():
|
|
|
|
|
raise NotImplemented()
|
|
|
|
|
else:
|
|
|
|
|
raise Exception("no such pagesize: %s" % args["pagesize_dropdown"].get())
|
|
|
|
|
imgsizearg = None
|
|
|
|
|
if args["imgsize_dropdown"].get() == "auto":
|
|
|
|
|
# nothing to do
|
|
|
|
|
pass
|
|
|
|
|
elif args["imgsize_dropdown"].get() == "custom":
|
|
|
|
|
imgsizearg = args["imgsize_width"].get(), args["imgsize_height"].get()
|
|
|
|
|
elif args["imgsize_dropdown"].get() in papernames.values():
|
|
|
|
|
raise NotImplemented()
|
|
|
|
|
else:
|
|
|
|
|
raise Exception("no such imgsize: %s" % args["imgsize_dropdown"].get())
|
|
|
|
|
borderarg = None
|
|
|
|
|
layout_fun = get_layout_fun(
|
|
|
|
|
pagesizearg,
|
|
|
|
|
imgsizearg,
|
|
|
|
|
borderarg,
|
|
|
|
|
args["fit"].get(),
|
|
|
|
|
args["auto_orient"].get(),
|
|
|
|
|
)
|
|
|
|
|
viewer_panesarg = None
|
|
|
|
|
if args["viewer_panes"].get() == "auto":
|
|
|
|
|
# nothing to do
|
|
|
|
|
pass
|
|
|
|
|
elif args["viewer_panes"].get() in PageMode:
|
|
|
|
|
viewer_panesarg = args["viewer_panes"].get()
|
|
|
|
|
else:
|
|
|
|
|
raise Exception("no such viewer_panes: %s" % args["viewer_panes"].get())
|
|
|
|
|
viewer_magnificationarg = None
|
|
|
|
|
if args["viewer_magnification"].get() == "auto":
|
|
|
|
|
# nothing to do
|
|
|
|
|
pass
|
|
|
|
|
elif args["viewer_magnification"].get() in Magnification:
|
|
|
|
|
viewer_magnificationarg = args["viewer_magnification"].get()
|
|
|
|
|
else:
|
|
|
|
|
raise Exception(
|
|
|
|
|
"no such viewer_magnification: %s" % args["viewer_magnification"].get()
|
2019-08-25 04:03:48 +00:00
|
|
|
|
)
|
2020-04-05 10:21:09 +00:00
|
|
|
|
viewer_page_layoutarg = None
|
|
|
|
|
if args["viewer_page_layout"].get() == "auto":
|
|
|
|
|
# nothing to do
|
|
|
|
|
pass
|
|
|
|
|
elif args["viewer_page_layout"].get() in PageLayout:
|
|
|
|
|
viewer_page_layoutarg = args["viewer_page_layout"].get()
|
|
|
|
|
else:
|
|
|
|
|
raise Exception(
|
|
|
|
|
"no such viewer_page_layout: %s" % args["viewer_page_layout"].get()
|
|
|
|
|
)
|
|
|
|
|
colorspacearg = None
|
|
|
|
|
if args["colorspace"].get() != "auto":
|
|
|
|
|
colorspacearg = next(
|
|
|
|
|
v for v in Colorspace if v.name == args["colorspace"].get()
|
|
|
|
|
)
|
2020-05-28 10:46:37 +00:00
|
|
|
|
enginearg = None
|
|
|
|
|
if args["engine"].get() != "auto":
|
|
|
|
|
enginearg = next(v for v in Engine if v.name == args["engine"].get())
|
2020-04-05 10:21:09 +00:00
|
|
|
|
|
|
|
|
|
convert(
|
|
|
|
|
*infiles,
|
2020-05-28 10:46:37 +00:00
|
|
|
|
engine=enginearg,
|
2020-04-05 10:21:09 +00:00
|
|
|
|
title=args["title"].get() if args["title"].get() else None,
|
|
|
|
|
author=args["author"].get() if args["author"].get() else None,
|
|
|
|
|
creator=args["creator"].get() if args["creator"].get() else None,
|
|
|
|
|
producer=args["producer"].get() if args["producer"].get() else None,
|
|
|
|
|
creationdate=args["creationdate"].get()
|
|
|
|
|
if args["creationdate"].get()
|
|
|
|
|
else None,
|
|
|
|
|
moddate=args["moddate"].get() if args["moddate"].get() else None,
|
|
|
|
|
subject=args["subject"].get() if args["subject"].get() else None,
|
|
|
|
|
keywords=args["keywords"].get() if args["keywords"].get() else None,
|
|
|
|
|
colorspace=colorspacearg,
|
|
|
|
|
nodate=args["nodate"].get(),
|
|
|
|
|
layout_fun=layout_fun,
|
|
|
|
|
viewer_panes=viewer_panesarg,
|
|
|
|
|
viewer_initial_page=args["viewer_initial_page"].get()
|
|
|
|
|
if args["viewer_initial_page"].get() > 1
|
|
|
|
|
else None,
|
|
|
|
|
viewer_magnification=viewer_magnificationarg,
|
|
|
|
|
viewer_page_layout=viewer_page_layoutarg,
|
|
|
|
|
viewer_fit_window=(args["viewer_fit_window"].get() or None),
|
|
|
|
|
viewer_center_window=(args["viewer_center_window"].get() or None),
|
|
|
|
|
viewer_fullscreen=(args["viewer_fullscreen"].get() or None),
|
|
|
|
|
outputstream=stream,
|
|
|
|
|
first_frame_only=args["first_frame_only"].get(),
|
|
|
|
|
cropborder=None,
|
|
|
|
|
bleedborder=None,
|
|
|
|
|
trimborder=None,
|
|
|
|
|
artborder=None,
|
2019-08-25 04:03:48 +00:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def on_save_button():
|
|
|
|
|
filename = tkinter.filedialog.asksaveasfilename(
|
|
|
|
|
parent=root,
|
|
|
|
|
title="save PDF",
|
|
|
|
|
defaultextension=".pdf",
|
|
|
|
|
filetypes=[("pdf documents", "*.pdf"), ("all files", "*")],
|
|
|
|
|
# initialdir="/home/josch/git/plakativ",
|
|
|
|
|
# initialfile=base + "_poster" + ext,
|
|
|
|
|
)
|
|
|
|
|
with open(filename, "wb") as f:
|
2020-04-05 10:21:09 +00:00
|
|
|
|
save_pdf(f)
|
2019-08-25 04:03:48 +00:00
|
|
|
|
|
|
|
|
|
root.title("img2pdf")
|
|
|
|
|
app.pack(fill=tkinter.BOTH, expand=tkinter.TRUE)
|
2020-04-05 10:21:09 +00:00
|
|
|
|
|
|
|
|
|
canvas = tkinter.Canvas(app, bg="black")
|
|
|
|
|
|
|
|
|
|
def draw():
|
|
|
|
|
canvas.delete(tkinter.ALL)
|
|
|
|
|
if not infiles:
|
|
|
|
|
canvas.create_text(
|
|
|
|
|
canvas.size[0] / 2,
|
|
|
|
|
canvas.size[1] / 2,
|
|
|
|
|
text='Click on the "Open Image(s)" button in the upper right.',
|
|
|
|
|
fill="white",
|
|
|
|
|
)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
if not doc:
|
|
|
|
|
canvas.create_text(
|
|
|
|
|
canvas.size[0] / 2,
|
|
|
|
|
canvas.size[1] / 2,
|
|
|
|
|
text="PyMuPDF not available. Install the Python fitz module\n"
|
|
|
|
|
+ "for preview functionality.",
|
|
|
|
|
fill="white",
|
|
|
|
|
)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
canvas_padding = 10
|
|
|
|
|
# factor to convert from pdf dimensions (given in pt) into canvas
|
|
|
|
|
# dimensions (given in pixels)
|
|
|
|
|
zoom = min(
|
|
|
|
|
(canvas.size[0] - canvas_padding) / maxpagewidth,
|
|
|
|
|
(canvas.size[1] - canvas_padding) / maxpageheight,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
pagenum = 0
|
|
|
|
|
mat_0 = fitz.Matrix(zoom, zoom)
|
|
|
|
|
canvas.image = tkinter.PhotoImage(
|
|
|
|
|
data=doc[pagenum]
|
|
|
|
|
.getDisplayList()
|
|
|
|
|
.getPixmap(matrix=mat_0, alpha=False)
|
|
|
|
|
.getImageData("ppm")
|
|
|
|
|
)
|
|
|
|
|
canvas.create_image(
|
|
|
|
|
(canvas.size[0] - maxpagewidth * zoom) / 2,
|
|
|
|
|
(canvas.size[1] - maxpageheight * zoom) / 2,
|
|
|
|
|
anchor=tkinter.NW,
|
|
|
|
|
image=canvas.image,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
canvas.create_rectangle(
|
|
|
|
|
(canvas.size[0] - maxpagewidth * zoom) / 2,
|
|
|
|
|
(canvas.size[1] - maxpageheight * zoom) / 2,
|
|
|
|
|
(canvas.size[0] - maxpagewidth * zoom) / 2 + canvas.image.width(),
|
|
|
|
|
(canvas.size[1] - maxpageheight * zoom) / 2 + canvas.image.height(),
|
|
|
|
|
outline="red",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def on_resize(event):
|
|
|
|
|
canvas.size = (event.width, event.height)
|
|
|
|
|
draw()
|
|
|
|
|
|
|
|
|
|
canvas.pack(fill=tkinter.BOTH, side=tkinter.LEFT, expand=tkinter.TRUE)
|
|
|
|
|
canvas.bind("<Configure>", on_resize)
|
|
|
|
|
|
|
|
|
|
frame_right = tkinter.Frame(app)
|
|
|
|
|
frame_right.pack(side=tkinter.TOP, expand=tkinter.TRUE, fill=tkinter.Y)
|
|
|
|
|
|
|
|
|
|
top_frame = tkinter.Frame(frame_right)
|
|
|
|
|
top_frame.pack(fill=tkinter.X)
|
|
|
|
|
|
|
|
|
|
tkinter.Button(top_frame, text="Open Image(s)", command=on_open_button).pack(
|
|
|
|
|
side=tkinter.LEFT, expand=tkinter.TRUE, fill=tkinter.X
|
|
|
|
|
)
|
|
|
|
|
tkinter.Button(top_frame, text="Help", state=tkinter.DISABLED).pack(
|
|
|
|
|
side=tkinter.RIGHT, expand=tkinter.TRUE, fill=tkinter.X
|
2019-08-25 04:03:48 +00:00
|
|
|
|
)
|
2020-04-05 10:21:09 +00:00
|
|
|
|
|
|
|
|
|
frame1 = VerticalScrolledFrame(frame_right)
|
|
|
|
|
frame1.pack(side=tkinter.TOP, expand=tkinter.TRUE, fill=tkinter.Y)
|
2019-08-25 04:03:48 +00:00
|
|
|
|
|
|
|
|
|
output_options = tkinter.LabelFrame(frame1.interior, text="Output Options")
|
|
|
|
|
output_options.pack(side=tkinter.TOP, expand=tkinter.TRUE, fill=tkinter.X)
|
2020-04-05 10:21:09 +00:00
|
|
|
|
tkinter.Label(output_options, text="colorspace").grid(
|
2019-08-25 04:03:48 +00:00
|
|
|
|
row=0, column=0, sticky=tkinter.W
|
|
|
|
|
)
|
2020-04-05 10:21:09 +00:00
|
|
|
|
OptionMenu(output_options, args["colorspace"], "auto", state=tkinter.DISABLED).grid(
|
2019-08-25 04:03:48 +00:00
|
|
|
|
row=0, column=1, sticky=tkinter.W
|
|
|
|
|
)
|
2020-05-28 10:46:37 +00:00
|
|
|
|
tkinter.Label(output_options, text="engine").grid(row=1, column=0, sticky=tkinter.W)
|
|
|
|
|
OptionMenu(output_options, args["engine"], "auto", state=tkinter.DISABLED).grid(
|
|
|
|
|
row=1, column=1, sticky=tkinter.W
|
|
|
|
|
)
|
2019-08-25 04:03:48 +00:00
|
|
|
|
tkinter.Checkbutton(
|
2020-04-05 10:21:09 +00:00
|
|
|
|
output_options,
|
|
|
|
|
text="Suppress timestamp",
|
|
|
|
|
variable=args["nodate"],
|
|
|
|
|
state=tkinter.DISABLED,
|
2019-08-25 04:03:48 +00:00
|
|
|
|
).grid(row=2, column=0, columnspan=2, sticky=tkinter.W)
|
|
|
|
|
tkinter.Checkbutton(
|
2020-04-05 10:21:09 +00:00
|
|
|
|
output_options,
|
|
|
|
|
text="only first frame",
|
|
|
|
|
variable=args["first_frame_only"],
|
|
|
|
|
state=tkinter.DISABLED,
|
2019-08-25 04:03:48 +00:00
|
|
|
|
).grid(row=3, column=0, columnspan=2, sticky=tkinter.W)
|
|
|
|
|
tkinter.Checkbutton(
|
|
|
|
|
output_options, text="force large input", state=tkinter.DISABLED
|
|
|
|
|
).grid(row=4, column=0, columnspan=2, sticky=tkinter.W)
|
|
|
|
|
image_size_frame = tkinter.LabelFrame(frame1.interior, text="Image size")
|
|
|
|
|
image_size_frame.pack(side=tkinter.TOP, expand=tkinter.TRUE, fill=tkinter.X)
|
2020-04-05 10:21:09 +00:00
|
|
|
|
OptionMenu(
|
|
|
|
|
image_size_frame,
|
|
|
|
|
args["imgsize_dropdown"],
|
|
|
|
|
*(["auto", "custom"] + sorted(papernames.values())),
|
|
|
|
|
state=tkinter.DISABLED,
|
|
|
|
|
).grid(row=1, column=0, columnspan=3, sticky=tkinter.W)
|
2019-08-25 04:03:48 +00:00
|
|
|
|
|
|
|
|
|
tkinter.Label(
|
|
|
|
|
image_size_frame, text="Width:", state=tkinter.DISABLED, name="size_label_width"
|
|
|
|
|
).grid(row=2, column=0, sticky=tkinter.W)
|
|
|
|
|
tkinter.Spinbox(
|
|
|
|
|
image_size_frame,
|
|
|
|
|
format="%.2f",
|
|
|
|
|
increment=0.01,
|
|
|
|
|
from_=0,
|
|
|
|
|
to=100,
|
|
|
|
|
width=5,
|
|
|
|
|
state=tkinter.DISABLED,
|
|
|
|
|
name="spinbox_width",
|
|
|
|
|
).grid(row=2, column=1, sticky=tkinter.W)
|
|
|
|
|
tkinter.Label(
|
|
|
|
|
image_size_frame, text="mm", state=tkinter.DISABLED, name="size_label_width_mm"
|
|
|
|
|
).grid(row=2, column=2, sticky=tkinter.W)
|
|
|
|
|
|
|
|
|
|
tkinter.Label(
|
|
|
|
|
image_size_frame,
|
|
|
|
|
text="Height:",
|
|
|
|
|
state=tkinter.DISABLED,
|
|
|
|
|
name="size_label_height",
|
|
|
|
|
).grid(row=3, column=0, sticky=tkinter.W)
|
|
|
|
|
tkinter.Spinbox(
|
|
|
|
|
image_size_frame,
|
|
|
|
|
format="%.2f",
|
|
|
|
|
increment=0.01,
|
|
|
|
|
from_=0,
|
|
|
|
|
to=100,
|
|
|
|
|
width=5,
|
|
|
|
|
state=tkinter.DISABLED,
|
|
|
|
|
name="spinbox_height",
|
|
|
|
|
).grid(row=3, column=1, sticky=tkinter.W)
|
|
|
|
|
tkinter.Label(
|
|
|
|
|
image_size_frame, text="mm", state=tkinter.DISABLED, name="size_label_height_mm"
|
|
|
|
|
).grid(row=3, column=2, sticky=tkinter.W)
|
|
|
|
|
|
|
|
|
|
page_size_frame = tkinter.LabelFrame(frame1.interior, text="Page size")
|
|
|
|
|
page_size_frame.pack(side=tkinter.TOP, expand=tkinter.TRUE, fill=tkinter.X)
|
2020-04-05 10:21:09 +00:00
|
|
|
|
OptionMenu(
|
|
|
|
|
page_size_frame,
|
|
|
|
|
args["pagesize_dropdown"],
|
|
|
|
|
*(["auto", "custom"] + sorted(papernames.values())),
|
|
|
|
|
state=tkinter.DISABLED,
|
|
|
|
|
).grid(row=1, column=0, columnspan=3, sticky=tkinter.W)
|
2019-08-25 04:03:48 +00:00
|
|
|
|
|
|
|
|
|
tkinter.Label(
|
|
|
|
|
page_size_frame, text="Width:", state=tkinter.DISABLED, name="size_label_width"
|
|
|
|
|
).grid(row=2, column=0, sticky=tkinter.W)
|
|
|
|
|
tkinter.Spinbox(
|
|
|
|
|
page_size_frame,
|
|
|
|
|
format="%.2f",
|
|
|
|
|
increment=0.01,
|
|
|
|
|
from_=0,
|
|
|
|
|
to=100,
|
|
|
|
|
width=5,
|
|
|
|
|
state=tkinter.DISABLED,
|
|
|
|
|
name="spinbox_width",
|
|
|
|
|
).grid(row=2, column=1, sticky=tkinter.W)
|
|
|
|
|
tkinter.Label(
|
|
|
|
|
page_size_frame, text="mm", state=tkinter.DISABLED, name="size_label_width_mm"
|
|
|
|
|
).grid(row=2, column=2, sticky=tkinter.W)
|
|
|
|
|
|
|
|
|
|
tkinter.Label(
|
|
|
|
|
page_size_frame,
|
|
|
|
|
text="Height:",
|
|
|
|
|
state=tkinter.DISABLED,
|
|
|
|
|
name="size_label_height",
|
|
|
|
|
).grid(row=3, column=0, sticky=tkinter.W)
|
|
|
|
|
tkinter.Spinbox(
|
|
|
|
|
page_size_frame,
|
|
|
|
|
format="%.2f",
|
|
|
|
|
increment=0.01,
|
|
|
|
|
from_=0,
|
|
|
|
|
to=100,
|
|
|
|
|
width=5,
|
|
|
|
|
state=tkinter.DISABLED,
|
|
|
|
|
name="spinbox_height",
|
|
|
|
|
).grid(row=3, column=1, sticky=tkinter.W)
|
|
|
|
|
tkinter.Label(
|
|
|
|
|
page_size_frame, text="mm", state=tkinter.DISABLED, name="size_label_height_mm"
|
|
|
|
|
).grid(row=3, column=2, sticky=tkinter.W)
|
|
|
|
|
layout_frame = tkinter.LabelFrame(frame1.interior, text="Layout")
|
|
|
|
|
layout_frame.pack(side=tkinter.TOP, expand=tkinter.TRUE, fill=tkinter.X)
|
|
|
|
|
tkinter.Label(layout_frame, text="border", state=tkinter.DISABLED).grid(
|
|
|
|
|
row=0, column=0, sticky=tkinter.W
|
|
|
|
|
)
|
|
|
|
|
tkinter.Spinbox(layout_frame, state=tkinter.DISABLED).grid(
|
|
|
|
|
row=0, column=1, sticky=tkinter.W
|
|
|
|
|
)
|
|
|
|
|
tkinter.Label(layout_frame, text="fit", state=tkinter.DISABLED).grid(
|
|
|
|
|
row=1, column=0, sticky=tkinter.W
|
|
|
|
|
)
|
2020-04-05 10:21:09 +00:00
|
|
|
|
OptionMenu(
|
|
|
|
|
layout_frame, args["fit"], *[v.name for v in FitMode], state=tkinter.DISABLED
|
|
|
|
|
).grid(row=1, column=1, sticky=tkinter.W)
|
|
|
|
|
tkinter.Checkbutton(
|
|
|
|
|
layout_frame,
|
|
|
|
|
text="auto orient",
|
|
|
|
|
state=tkinter.DISABLED,
|
|
|
|
|
variable=args["auto_orient"],
|
|
|
|
|
).grid(row=2, column=0, columnspan=2, sticky=tkinter.W)
|
2019-08-25 04:03:48 +00:00
|
|
|
|
tkinter.Label(layout_frame, text="crop border", state=tkinter.DISABLED).grid(
|
|
|
|
|
row=3, column=0, sticky=tkinter.W
|
|
|
|
|
)
|
|
|
|
|
tkinter.Spinbox(layout_frame, state=tkinter.DISABLED).grid(
|
|
|
|
|
row=3, column=1, sticky=tkinter.W
|
|
|
|
|
)
|
|
|
|
|
tkinter.Label(layout_frame, text="bleed border", state=tkinter.DISABLED).grid(
|
|
|
|
|
row=4, column=0, sticky=tkinter.W
|
|
|
|
|
)
|
|
|
|
|
tkinter.Spinbox(layout_frame, state=tkinter.DISABLED).grid(
|
|
|
|
|
row=4, column=1, sticky=tkinter.W
|
|
|
|
|
)
|
|
|
|
|
tkinter.Label(layout_frame, text="trim border", state=tkinter.DISABLED).grid(
|
|
|
|
|
row=5, column=0, sticky=tkinter.W
|
|
|
|
|
)
|
|
|
|
|
tkinter.Spinbox(layout_frame, state=tkinter.DISABLED).grid(
|
|
|
|
|
row=5, column=1, sticky=tkinter.W
|
|
|
|
|
)
|
|
|
|
|
tkinter.Label(layout_frame, text="art border", state=tkinter.DISABLED).grid(
|
|
|
|
|
row=6, column=0, sticky=tkinter.W
|
|
|
|
|
)
|
|
|
|
|
tkinter.Spinbox(layout_frame, state=tkinter.DISABLED).grid(
|
|
|
|
|
row=6, column=1, sticky=tkinter.W
|
|
|
|
|
)
|
|
|
|
|
metadata_frame = tkinter.LabelFrame(frame1.interior, text="PDF metadata")
|
|
|
|
|
metadata_frame.pack(side=tkinter.TOP, expand=tkinter.TRUE, fill=tkinter.X)
|
|
|
|
|
tkinter.Label(metadata_frame, text="title", state=tkinter.DISABLED).grid(
|
|
|
|
|
row=0, column=0, sticky=tkinter.W
|
|
|
|
|
)
|
2020-04-05 10:21:09 +00:00
|
|
|
|
tkinter.Entry(
|
|
|
|
|
metadata_frame, textvariable=args["title"], state=tkinter.DISABLED
|
|
|
|
|
).grid(row=0, column=1, sticky=tkinter.W)
|
2019-08-25 04:03:48 +00:00
|
|
|
|
tkinter.Label(metadata_frame, text="author", state=tkinter.DISABLED).grid(
|
|
|
|
|
row=1, column=0, sticky=tkinter.W
|
|
|
|
|
)
|
2020-04-05 10:21:09 +00:00
|
|
|
|
tkinter.Entry(
|
|
|
|
|
metadata_frame, textvariable=args["author"], state=tkinter.DISABLED
|
|
|
|
|
).grid(row=1, column=1, sticky=tkinter.W)
|
2019-08-25 04:03:48 +00:00
|
|
|
|
tkinter.Label(metadata_frame, text="creator", state=tkinter.DISABLED).grid(
|
|
|
|
|
row=2, column=0, sticky=tkinter.W
|
|
|
|
|
)
|
2020-04-05 10:21:09 +00:00
|
|
|
|
tkinter.Entry(
|
|
|
|
|
metadata_frame, textvariable=args["creator"], state=tkinter.DISABLED
|
|
|
|
|
).grid(row=2, column=1, sticky=tkinter.W)
|
2019-08-25 04:03:48 +00:00
|
|
|
|
tkinter.Label(metadata_frame, text="producer", state=tkinter.DISABLED).grid(
|
|
|
|
|
row=3, column=0, sticky=tkinter.W
|
|
|
|
|
)
|
2020-04-05 10:21:09 +00:00
|
|
|
|
tkinter.Entry(
|
|
|
|
|
metadata_frame, textvariable=args["producer"], state=tkinter.DISABLED
|
|
|
|
|
).grid(row=3, column=1, sticky=tkinter.W)
|
2019-08-25 04:03:48 +00:00
|
|
|
|
tkinter.Label(metadata_frame, text="creation date", state=tkinter.DISABLED).grid(
|
|
|
|
|
row=4, column=0, sticky=tkinter.W
|
|
|
|
|
)
|
2020-04-05 10:21:09 +00:00
|
|
|
|
tkinter.Entry(
|
|
|
|
|
metadata_frame, textvariable=args["creationdate"], state=tkinter.DISABLED
|
|
|
|
|
).grid(row=4, column=1, sticky=tkinter.W)
|
2019-08-25 04:03:48 +00:00
|
|
|
|
tkinter.Label(
|
|
|
|
|
metadata_frame, text="modification date", state=tkinter.DISABLED
|
|
|
|
|
).grid(row=5, column=0, sticky=tkinter.W)
|
2020-04-05 10:21:09 +00:00
|
|
|
|
tkinter.Entry(
|
|
|
|
|
metadata_frame, textvariable=args["moddate"], state=tkinter.DISABLED
|
|
|
|
|
).grid(row=5, column=1, sticky=tkinter.W)
|
2019-08-25 04:03:48 +00:00
|
|
|
|
tkinter.Label(metadata_frame, text="subject", state=tkinter.DISABLED).grid(
|
|
|
|
|
row=6, column=0, sticky=tkinter.W
|
|
|
|
|
)
|
|
|
|
|
tkinter.Entry(metadata_frame, state=tkinter.DISABLED).grid(
|
|
|
|
|
row=6, column=1, sticky=tkinter.W
|
|
|
|
|
)
|
|
|
|
|
tkinter.Label(metadata_frame, text="keywords", state=tkinter.DISABLED).grid(
|
|
|
|
|
row=7, column=0, sticky=tkinter.W
|
|
|
|
|
)
|
|
|
|
|
tkinter.Entry(metadata_frame, state=tkinter.DISABLED).grid(
|
|
|
|
|
row=7, column=1, sticky=tkinter.W
|
|
|
|
|
)
|
|
|
|
|
viewer_frame = tkinter.LabelFrame(frame1.interior, text="PDF viewer options")
|
|
|
|
|
viewer_frame.pack(side=tkinter.TOP, expand=tkinter.TRUE, fill=tkinter.X)
|
|
|
|
|
tkinter.Label(viewer_frame, text="panes", state=tkinter.DISABLED).grid(
|
|
|
|
|
row=0, column=0, sticky=tkinter.W
|
|
|
|
|
)
|
2020-04-05 10:21:09 +00:00
|
|
|
|
OptionMenu(
|
|
|
|
|
viewer_frame,
|
|
|
|
|
args["viewer_panes"],
|
|
|
|
|
*(["auto"] + [v.name for v in PageMode]),
|
|
|
|
|
state=tkinter.DISABLED,
|
|
|
|
|
).grid(row=0, column=1, sticky=tkinter.W)
|
2019-08-25 04:03:48 +00:00
|
|
|
|
tkinter.Label(viewer_frame, text="initial page", state=tkinter.DISABLED).grid(
|
|
|
|
|
row=1, column=0, sticky=tkinter.W
|
|
|
|
|
)
|
2020-04-05 10:21:09 +00:00
|
|
|
|
tkinter.Spinbox(
|
|
|
|
|
viewer_frame,
|
|
|
|
|
increment=1,
|
|
|
|
|
from_=1,
|
|
|
|
|
to=10000,
|
|
|
|
|
width=6,
|
|
|
|
|
textvariable=args["viewer_initial_page"],
|
|
|
|
|
state=tkinter.DISABLED,
|
|
|
|
|
name="viewer_initial_page_spinbox",
|
|
|
|
|
).grid(row=1, column=1, sticky=tkinter.W)
|
2019-08-25 04:03:48 +00:00
|
|
|
|
tkinter.Label(viewer_frame, text="magnification", state=tkinter.DISABLED).grid(
|
|
|
|
|
row=2, column=0, sticky=tkinter.W
|
|
|
|
|
)
|
2020-04-05 10:21:09 +00:00
|
|
|
|
OptionMenu(
|
|
|
|
|
viewer_frame,
|
|
|
|
|
args["viewer_magnification"],
|
|
|
|
|
*(["auto", "custom"] + [v.name for v in Magnification]),
|
|
|
|
|
state=tkinter.DISABLED,
|
|
|
|
|
).grid(row=2, column=1, sticky=tkinter.W)
|
2019-08-25 04:03:48 +00:00
|
|
|
|
tkinter.Label(viewer_frame, text="page layout", state=tkinter.DISABLED).grid(
|
|
|
|
|
row=3, column=0, sticky=tkinter.W
|
|
|
|
|
)
|
2020-04-05 10:21:09 +00:00
|
|
|
|
OptionMenu(
|
|
|
|
|
viewer_frame,
|
|
|
|
|
args["viewer_page_layout"],
|
|
|
|
|
*(["auto"] + [v.name for v in PageLayout]),
|
|
|
|
|
state=tkinter.DISABLED,
|
|
|
|
|
).grid(row=3, column=1, sticky=tkinter.W)
|
2019-08-25 04:03:48 +00:00
|
|
|
|
tkinter.Checkbutton(
|
2020-04-05 10:21:09 +00:00
|
|
|
|
viewer_frame,
|
|
|
|
|
text="fit window to page size",
|
|
|
|
|
variable=args["viewer_fit_window"],
|
|
|
|
|
state=tkinter.DISABLED,
|
2019-08-25 04:03:48 +00:00
|
|
|
|
).grid(row=4, column=0, columnspan=2, sticky=tkinter.W)
|
|
|
|
|
tkinter.Checkbutton(
|
2020-04-05 10:21:09 +00:00
|
|
|
|
viewer_frame,
|
|
|
|
|
text="center window",
|
|
|
|
|
variable=args["viewer_center_window"],
|
|
|
|
|
state=tkinter.DISABLED,
|
2019-08-25 04:03:48 +00:00
|
|
|
|
).grid(row=5, column=0, columnspan=2, sticky=tkinter.W)
|
|
|
|
|
tkinter.Checkbutton(
|
2020-04-05 10:21:09 +00:00
|
|
|
|
viewer_frame,
|
|
|
|
|
text="open in fullscreen",
|
|
|
|
|
variable=args["viewer_fullscreen"],
|
|
|
|
|
state=tkinter.DISABLED,
|
2019-08-25 04:03:48 +00:00
|
|
|
|
).grid(row=6, column=0, columnspan=2, sticky=tkinter.W)
|
|
|
|
|
|
|
|
|
|
option_frame = tkinter.LabelFrame(frame1.interior, text="Program options")
|
|
|
|
|
option_frame.pack(side=tkinter.TOP, expand=tkinter.TRUE, fill=tkinter.X)
|
|
|
|
|
|
|
|
|
|
tkinter.Label(option_frame, text="Unit:", state=tkinter.DISABLED).grid(
|
|
|
|
|
row=0, column=0, sticky=tkinter.W
|
|
|
|
|
)
|
|
|
|
|
unit = tkinter.StringVar()
|
|
|
|
|
unit.set("mm")
|
|
|
|
|
OptionMenu(option_frame, unit, ["mm"], state=tkinter.DISABLED).grid(
|
|
|
|
|
row=0, column=1, sticky=tkinter.W
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
tkinter.Label(option_frame, text="Language:", state=tkinter.DISABLED).grid(
|
|
|
|
|
row=1, column=0, sticky=tkinter.W
|
|
|
|
|
)
|
|
|
|
|
language = tkinter.StringVar()
|
|
|
|
|
language.set("English")
|
|
|
|
|
OptionMenu(option_frame, language, ["English"], state=tkinter.DISABLED).grid(
|
|
|
|
|
row=1, column=1, sticky=tkinter.W
|
|
|
|
|
)
|
|
|
|
|
|
2020-04-05 10:21:09 +00:00
|
|
|
|
bottom_frame = tkinter.Frame(frame_right)
|
|
|
|
|
bottom_frame.pack(fill=tkinter.X)
|
|
|
|
|
|
|
|
|
|
tkinter.Button(bottom_frame, text="Save PDF", command=on_save_button).pack(
|
|
|
|
|
side=tkinter.LEFT, expand=tkinter.TRUE, fill=tkinter.X
|
|
|
|
|
)
|
|
|
|
|
tkinter.Button(bottom_frame, text="Exit", command=root.destroy).pack(
|
|
|
|
|
side=tkinter.RIGHT, expand=tkinter.TRUE, fill=tkinter.X
|
2019-08-25 04:03:48 +00:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
app.mainloop()
|
|
|
|
|
|
|
|
|
|
|
2017-08-07 09:21:42 +00:00
|
|
|
|
def main(argv=sys.argv):
|
2015-05-10 12:35:09 +00:00
|
|
|
|
rendered_papersizes = ""
|
|
|
|
|
for k, v in sorted(papersizes.items()):
|
|
|
|
|
rendered_papersizes += " %-8s %s\n" % (papernames[k], v)
|
|
|
|
|
|
|
|
|
|
parser = argparse.ArgumentParser(
|
2018-12-25 11:30:10 +00:00
|
|
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
|
|
|
description="""\
|
2018-03-24 18:53:41 +00:00
|
|
|
|
Losslessly convert raster images to PDF without re-encoding PNG, JPEG, and
|
|
|
|
|
JPEG2000 images. This leads to a lossless conversion of PNG, JPEG and JPEG2000
|
|
|
|
|
images with the only added file size coming from the PDF container itself.
|
|
|
|
|
Other raster graphics formats are losslessly stored using the same encoding
|
|
|
|
|
that PNG uses. Since PDF does not support images with transparency and since
|
|
|
|
|
img2pdf aims to never be lossy, input images with an alpha channel are not
|
|
|
|
|
supported.
|
2015-05-10 12:35:09 +00:00
|
|
|
|
|
|
|
|
|
The output is sent to standard output so that it can be redirected into a file
|
|
|
|
|
or to another program as part of a shell pipe. To directly write the output
|
|
|
|
|
into a file, use the -o or --output option.
|
2017-01-20 04:17:23 +00:00
|
|
|
|
|
|
|
|
|
Options:
|
2018-12-25 11:30:10 +00:00
|
|
|
|
""",
|
|
|
|
|
epilog="""\
|
2017-01-20 04:17:23 +00:00
|
|
|
|
Colorspace:
|
2015-05-10 12:35:09 +00:00
|
|
|
|
Currently, the colorspace must be forced for JPEG 2000 images that are not in
|
|
|
|
|
the RGB colorspace. Available colorspace options are based on Python Imaging
|
|
|
|
|
Library (PIL) short handles.
|
|
|
|
|
|
|
|
|
|
RGB RGB color
|
|
|
|
|
L Grayscale
|
|
|
|
|
1 Black and white (internally converted to grayscale)
|
|
|
|
|
CMYK CMYK color
|
|
|
|
|
CMYK;I CMYK color with inversion (for CMYK JPEG files from Adobe)
|
|
|
|
|
|
2017-01-20 04:17:23 +00:00
|
|
|
|
Paper sizes:
|
2015-05-10 12:35:09 +00:00
|
|
|
|
You can specify the short hand paper size names shown in the first column in
|
|
|
|
|
the table below as arguments to the --pagesize and --imgsize options. The
|
|
|
|
|
width and height they are mapping to is shown in the second column. Giving
|
|
|
|
|
the value in the second column has the same effect as giving the short hand
|
|
|
|
|
in the first column. Appending ^T (a caret/circumflex followed by the letter
|
|
|
|
|
T) turns the paper size from portrait into landscape. The postfix thus
|
|
|
|
|
symbolizes the transpose. The values are case insensitive.
|
|
|
|
|
|
|
|
|
|
%s
|
|
|
|
|
|
2017-01-20 04:17:23 +00:00
|
|
|
|
Fit options:
|
2015-05-10 12:35:09 +00:00
|
|
|
|
The img2pdf options for the --fit argument are shown in the first column in
|
|
|
|
|
the table below. The function of these options can be mapped to the geometry
|
|
|
|
|
operators of imagemagick. For users who are familiar with imagemagick, the
|
|
|
|
|
corresponding operator is shown in the second column. The third column shows
|
|
|
|
|
whether or not the aspect ratio is preserved for that option (same as in
|
|
|
|
|
imagemagick). Just like imagemagick, img2pdf tries hard to preserve the
|
|
|
|
|
aspect ratio, so if the --fit argument is not given, then the default is
|
|
|
|
|
"into" which corresponds to the absence of any operator in imagemagick.
|
|
|
|
|
The value of the --fit option is case insensitive.
|
|
|
|
|
|
|
|
|
|
into | | Y | The default. Width and height values specify maximum
|
|
|
|
|
| | | values.
|
|
|
|
|
---------+---+---+----------------------------------------------------------
|
|
|
|
|
fill | ^ | Y | Width and height values specify the minimum values.
|
|
|
|
|
---------+---+---+----------------------------------------------------------
|
|
|
|
|
exact | ! | N | Width and height emphatically given.
|
|
|
|
|
---------+---+---+----------------------------------------------------------
|
|
|
|
|
shrink | > | Y | Shrinks an image with dimensions larger than the given
|
|
|
|
|
| | | ones (and otherwise behaves like "into").
|
|
|
|
|
---------+---+---+----------------------------------------------------------
|
|
|
|
|
enlarge | < | Y | Enlarges an image with dimensions smaller than the given
|
|
|
|
|
| | | ones (and otherwise behaves like "into").
|
|
|
|
|
|
2017-01-20 04:17:23 +00:00
|
|
|
|
Argument parsing:
|
|
|
|
|
Argument long options can be abbreviated to a prefix if the abbreviation is
|
2018-07-19 10:05:27 +00:00
|
|
|
|
unambiguous. That is, the prefix must match a unique option.
|
2017-01-20 04:17:23 +00:00
|
|
|
|
|
|
|
|
|
Beware of your shell interpreting argument values as special characters (like
|
|
|
|
|
the semicolon in the CMYK;I colorspace option). If in doubt, put the argument
|
|
|
|
|
values in single quotes.
|
|
|
|
|
|
|
|
|
|
If you want an argument value to start with one or more minus characters, you
|
|
|
|
|
must use the long option name and join them with an equal sign like so:
|
|
|
|
|
|
|
|
|
|
$ img2pdf --author=--test--
|
|
|
|
|
|
|
|
|
|
If your input file name starts with one or more minus characters, either
|
|
|
|
|
separate the input files from the other arguments by two minus signs:
|
|
|
|
|
|
|
|
|
|
$ img2pdf -- --my-file-starts-with-two-minuses.jpg
|
2015-05-10 12:35:09 +00:00
|
|
|
|
|
2017-01-20 04:17:23 +00:00
|
|
|
|
Or be more explicit about its relative path by prepending a ./:
|
|
|
|
|
|
|
|
|
|
$ img2pdf ./--my-file-starts-with-two-minuses.jpg
|
|
|
|
|
|
|
|
|
|
The order of non-positional arguments (all arguments other than the input
|
|
|
|
|
images) does not matter.
|
|
|
|
|
|
|
|
|
|
Examples:
|
2015-05-10 12:35:09 +00:00
|
|
|
|
Lines starting with a dollar sign denote commands you can enter into your
|
|
|
|
|
terminal. The dollar sign signifies your command prompt. It is not part of
|
|
|
|
|
the command you type.
|
|
|
|
|
|
|
|
|
|
Convert two scans in JPEG format to a PDF document.
|
|
|
|
|
|
|
|
|
|
$ img2pdf --output out.pdf page1.jpg page2.jpg
|
|
|
|
|
|
|
|
|
|
Convert a directory of JPEG images into a PDF with printable A4 pages in
|
|
|
|
|
landscape mode. On each page, the photo takes the maximum amount of space
|
|
|
|
|
while preserving its aspect ratio and a print border of 2 cm on the top and
|
|
|
|
|
bottom and 2.5 cm on the left and right hand side.
|
|
|
|
|
|
|
|
|
|
$ img2pdf --output out.pdf --pagesize A4^T --border 2cm:2.5cm *.jpg
|
|
|
|
|
|
|
|
|
|
On each A4 page, fit images into a 10 cm times 15 cm rectangle but keep the
|
|
|
|
|
original image size if the image is smaller than that.
|
|
|
|
|
|
|
|
|
|
$ img2pdf --output out.pdf -S A4 --imgsize 10cmx15cm --fit shrink *.jpg
|
|
|
|
|
|
|
|
|
|
Prepare a directory of photos to be printed borderless on photo paper with a
|
|
|
|
|
3:2 aspect ratio and rotate each page so that its orientation is the same as
|
|
|
|
|
the input image.
|
|
|
|
|
|
|
|
|
|
$ img2pdf --output out.pdf --pagesize 15cmx10cm --auto-orient *.jpg
|
|
|
|
|
|
|
|
|
|
Encode a grayscale JPEG2000 image. The colorspace has to be forced as img2pdf
|
|
|
|
|
cannot read it from the JPEG2000 file automatically.
|
|
|
|
|
|
|
|
|
|
$ img2pdf --output out.pdf --colorspace L input.jp2
|
|
|
|
|
|
2017-01-20 04:17:23 +00:00
|
|
|
|
Written by Johannes 'josch' Schauer <josch@mister-muffin.de>
|
2015-05-10 12:35:09 +00:00
|
|
|
|
|
2017-01-20 04:17:23 +00:00
|
|
|
|
Report bugs at https://gitlab.mister-muffin.de/josch/img2pdf/issues
|
2018-12-25 11:30:10 +00:00
|
|
|
|
"""
|
|
|
|
|
% rendered_papersizes,
|
|
|
|
|
)
|
2015-05-10 12:35:09 +00:00
|
|
|
|
|
|
|
|
|
parser.add_argument(
|
2018-12-25 11:30:10 +00:00
|
|
|
|
"images",
|
|
|
|
|
metavar="infile",
|
|
|
|
|
type=input_images,
|
|
|
|
|
nargs="*",
|
|
|
|
|
help="Specifies the input file(s) in any format that can be read by "
|
|
|
|
|
"the Python Imaging Library (PIL). If no input images are given, then "
|
2015-05-10 12:35:09 +00:00
|
|
|
|
'a single image is read from standard input. The special filename "-" '
|
2018-12-25 11:30:10 +00:00
|
|
|
|
"can be used once to read an image from standard input. To read a "
|
2021-05-07 08:58:46 +00:00
|
|
|
|
'file in the current directory with the filename "-" (or with a '
|
|
|
|
|
'filename starting with "-"), pass it to img2pdf by explicitly '
|
|
|
|
|
'stating its relative path like "./-". Cannot be used together with '
|
|
|
|
|
"--from-file.",
|
2018-12-25 11:30:10 +00:00
|
|
|
|
)
|
2015-05-10 12:35:09 +00:00
|
|
|
|
parser.add_argument(
|
2018-12-25 11:30:10 +00:00
|
|
|
|
"-v",
|
|
|
|
|
"--verbose",
|
|
|
|
|
action="store_true",
|
|
|
|
|
help="Makes the program operate in verbose mode, printing messages on "
|
|
|
|
|
"standard error.",
|
|
|
|
|
)
|
2015-05-10 12:35:09 +00:00
|
|
|
|
parser.add_argument(
|
2018-12-25 11:30:10 +00:00
|
|
|
|
"-V",
|
|
|
|
|
"--version",
|
|
|
|
|
action="version",
|
|
|
|
|
version="%(prog)s " + __version__,
|
|
|
|
|
help="Prints version information and exits.",
|
|
|
|
|
)
|
2020-04-23 05:09:13 +00:00
|
|
|
|
parser.add_argument(
|
2020-04-23 05:13:39 +00:00
|
|
|
|
"--gui", dest="gui", action="store_true", help="run experimental tkinter gui"
|
2019-08-25 04:03:48 +00:00
|
|
|
|
)
|
2021-05-07 08:58:46 +00:00
|
|
|
|
parser.add_argument(
|
|
|
|
|
"--from-file",
|
|
|
|
|
metavar="FILE",
|
|
|
|
|
type=from_file,
|
|
|
|
|
default=[],
|
|
|
|
|
help="Read the list of images from FILE instead of passing them as "
|
|
|
|
|
"positional arguments. If this option is used, then the list of "
|
|
|
|
|
"positional arguments must be empty. The paths to the input images "
|
|
|
|
|
'in FILE are separated by NUL bytes. If FILE is "-" then the paths '
|
|
|
|
|
"are expected on standard input. This option is useful if you want "
|
|
|
|
|
"to pass more images than the maximum command length of your shell "
|
|
|
|
|
"permits. This option can be used with commands like `find -print0`.",
|
|
|
|
|
)
|
2015-05-10 12:35:09 +00:00
|
|
|
|
|
|
|
|
|
outargs = parser.add_argument_group(
|
2018-12-25 11:30:10 +00:00
|
|
|
|
title="General output arguments",
|
|
|
|
|
description="Arguments controlling the output format.",
|
|
|
|
|
)
|
2015-05-10 12:35:09 +00:00
|
|
|
|
|
2018-12-24 17:03:52 +00:00
|
|
|
|
# In Python3 we have to output to sys.stdout.buffer because we write are
|
|
|
|
|
# bytes and not strings. In certain situations, like when the main
|
|
|
|
|
# function is wrapped by contextlib.redirect_stdout(), sys.stdout does not
|
|
|
|
|
# have the buffer attribute. Thus we write to sys.stdout by default and
|
|
|
|
|
# to sys.stdout.buffer if it exists.
|
2015-05-10 12:35:09 +00:00
|
|
|
|
outargs.add_argument(
|
2018-12-25 11:30:10 +00:00
|
|
|
|
"-o",
|
|
|
|
|
"--output",
|
|
|
|
|
metavar="out",
|
|
|
|
|
type=argparse.FileType("wb"),
|
2018-12-24 17:03:52 +00:00
|
|
|
|
default=sys.stdout.buffer if hasattr(sys.stdout, "buffer") else sys.stdout,
|
2018-12-25 11:30:10 +00:00
|
|
|
|
help="Makes the program output to a file instead of standard output.",
|
|
|
|
|
)
|
2015-05-10 12:35:09 +00:00
|
|
|
|
outargs.add_argument(
|
2018-12-25 11:30:10 +00:00
|
|
|
|
"-C",
|
|
|
|
|
"--colorspace",
|
|
|
|
|
metavar="colorspace",
|
|
|
|
|
type=parse_colorspacearg,
|
|
|
|
|
help="""
|
2015-05-10 12:35:09 +00:00
|
|
|
|
Forces the PIL colorspace. See the epilogue for a list of possible values.
|
|
|
|
|
Usually the PDF colorspace would be derived from the color space of the input
|
|
|
|
|
image. This option overwrites the automatically detected colorspace from the
|
|
|
|
|
input image and thus forces a certain colorspace in the output PDF /ColorSpace
|
|
|
|
|
property. This is useful for JPEG 2000 images with a different colorspace than
|
2018-12-25 11:30:10 +00:00
|
|
|
|
RGB.""",
|
|
|
|
|
)
|
2015-05-10 12:35:09 +00:00
|
|
|
|
|
|
|
|
|
outargs.add_argument(
|
2018-12-25 11:30:10 +00:00
|
|
|
|
"-D",
|
|
|
|
|
"--nodate",
|
|
|
|
|
action="store_true",
|
|
|
|
|
help="Suppresses timestamps in the output and thus makes the output "
|
|
|
|
|
"deterministic between individual runs. You can also manually "
|
|
|
|
|
"set a date using the --moddate and --creationdate options.",
|
|
|
|
|
)
|
2015-05-10 12:35:09 +00:00
|
|
|
|
|
|
|
|
|
outargs.add_argument(
|
2020-05-28 10:46:37 +00:00
|
|
|
|
"--engine",
|
|
|
|
|
metavar="engine",
|
|
|
|
|
type=parse_enginearg,
|
2020-05-31 15:18:02 +00:00
|
|
|
|
help="Choose PDF engine. Can be either internal, pikepdf or pdfrw. "
|
|
|
|
|
"The internal engine does not have additional requirements and writes "
|
|
|
|
|
"out a human readable PDF. The pikepdf engine requires the pikepdf "
|
|
|
|
|
"Python module and qpdf library, is most featureful, can "
|
2020-06-01 00:10:07 +00:00
|
|
|
|
'linearize PDFs ("fast web view") and can compress more parts of it.'
|
2020-05-31 15:18:02 +00:00
|
|
|
|
"The pdfrw engine requires the pdfrw Python "
|
2020-05-28 10:46:37 +00:00
|
|
|
|
"module but does not support unicode metadata (See "
|
|
|
|
|
"https://github.com/pmaupin/pdfrw/issues/39) or palette data (See "
|
|
|
|
|
"https://github.com/pmaupin/pdfrw/issues/128).",
|
2018-12-25 11:30:10 +00:00
|
|
|
|
)
|
2015-05-10 12:35:09 +00:00
|
|
|
|
|
2016-02-16 23:30:15 +00:00
|
|
|
|
outargs.add_argument(
|
2018-12-25 11:30:10 +00:00
|
|
|
|
"--first-frame-only",
|
|
|
|
|
action="store_true",
|
2016-02-16 23:30:15 +00:00
|
|
|
|
help="By default, img2pdf will convert multi-frame images like "
|
2018-12-25 11:30:10 +00:00
|
|
|
|
"multi-page TIFF or animated GIF images to one page per frame. "
|
|
|
|
|
"This option will only let the first frame of every multi-frame "
|
|
|
|
|
"input image be converted into a page in the resulting PDF.",
|
|
|
|
|
)
|
2016-02-16 23:30:15 +00:00
|
|
|
|
|
2018-03-24 18:47:03 +00:00
|
|
|
|
outargs.add_argument(
|
2018-12-25 11:30:10 +00:00
|
|
|
|
"--pillow-limit-break",
|
|
|
|
|
action="store_true",
|
2018-03-24 18:47:03 +00:00
|
|
|
|
help="img2pdf uses the Python Imaging Library Pillow to read input "
|
2018-12-25 11:30:10 +00:00
|
|
|
|
"images. Pillow limits the maximum input image size to %d pixels "
|
|
|
|
|
"to prevent decompression bomb denial of service attacks. If "
|
|
|
|
|
"your input image contains more pixels than that, use this "
|
|
|
|
|
"option to disable this safety measure during this run of img2pdf"
|
|
|
|
|
% Image.MAX_IMAGE_PIXELS,
|
|
|
|
|
)
|
2018-03-24 18:47:03 +00:00
|
|
|
|
|
2020-06-01 00:10:07 +00:00
|
|
|
|
outargs.add_argument(
|
|
|
|
|
"--pdfa",
|
|
|
|
|
nargs="?",
|
2020-08-06 22:20:18 +00:00
|
|
|
|
const="/usr/share/color/icc/sRGB.icc",
|
2020-06-01 00:10:07 +00:00
|
|
|
|
default=None,
|
2020-08-27 00:48:26 +00:00
|
|
|
|
help="Output a PDF/A-1b compliant document. By default, this will "
|
2020-08-06 22:20:18 +00:00
|
|
|
|
"embed /usr/share/color/icc/sRGB.icc as the color profile.",
|
2020-06-01 00:10:07 +00:00
|
|
|
|
)
|
|
|
|
|
|
2015-05-10 12:35:09 +00:00
|
|
|
|
sizeargs = parser.add_argument_group(
|
2018-12-25 11:30:10 +00:00
|
|
|
|
title="Image and page size and layout arguments",
|
|
|
|
|
description="""\
|
2017-01-20 04:17:23 +00:00
|
|
|
|
Every input image will be placed on its own page. The image size is controlled
|
2015-05-10 12:35:09 +00:00
|
|
|
|
by the dpi value of the input image or, if unset or missing, the default dpi of
|
|
|
|
|
%.2f. By default, each page will have the same size as the image it shows.
|
|
|
|
|
Thus, there will be no visible border between the image and the page border by
|
|
|
|
|
default. If image size and page size are made different from each other by the
|
|
|
|
|
options in this section, the image will always be centered in both dimensions.
|
|
|
|
|
|
|
|
|
|
The image size and page size can be explicitly set using the --imgsize and
|
|
|
|
|
--pagesize options, respectively. If either dimension of the image size is
|
|
|
|
|
specified but the same dimension of the page size is not, then the latter will
|
|
|
|
|
be derived from the former using an optional minimal distance between the image
|
|
|
|
|
and the page border (given by the --border option) and/or a certain fitting
|
|
|
|
|
strategy (given by the --fit option). The converse happens if a dimension of
|
|
|
|
|
the page size is set but the same dimension of the image size is not.
|
|
|
|
|
|
|
|
|
|
Any length value in below options is represented by the meta variable L which
|
|
|
|
|
is a floating point value with an optional unit appended (without a space
|
|
|
|
|
between them). The default unit is pt (1/72 inch, the PDF unit) and other
|
|
|
|
|
allowed units are cm (centimeter), mm (millimeter), and in (inch).
|
|
|
|
|
|
|
|
|
|
Any size argument of the format LxL in the options below specifies the width
|
|
|
|
|
and height of a rectangle where the first L represents the width and the second
|
|
|
|
|
L represents the height with an optional unit following each value as described
|
2016-02-17 17:16:17 +00:00
|
|
|
|
above. Either width or height may be omitted. If the height is omitted, the
|
|
|
|
|
separating x can be omitted as well. Omitting the width requires to prefix the
|
|
|
|
|
height with the separating x. The missing dimension will be chosen so to not
|
|
|
|
|
change the image aspect ratio. Instead of giving the width and height
|
|
|
|
|
explicitly, you may also specify some (case-insensitive) common page sizes such
|
|
|
|
|
as letter and A4. See the epilogue at the bottom for a complete list of the
|
|
|
|
|
valid sizes.
|
2015-05-10 12:35:09 +00:00
|
|
|
|
|
|
|
|
|
The --fit option scales to fit the image into a rectangle that is either
|
2016-06-10 14:39:45 +00:00
|
|
|
|
derived from the --imgsize option or otherwise from the --pagesize option.
|
2015-05-10 12:35:09 +00:00
|
|
|
|
If the --border option is given in addition to the --imgsize option while the
|
|
|
|
|
--pagesize option is not given, then the page size will be calculated from the
|
|
|
|
|
image size, respecting the border setting. If the --border option is given in
|
|
|
|
|
addition to the --pagesize option while the --imgsize option is not given, then
|
|
|
|
|
the image size will be calculated from the page size, respecting the border
|
|
|
|
|
setting. If the --border option is given while both the --pagesize and
|
|
|
|
|
--imgsize options are passed, then the --border option will be ignored.
|
|
|
|
|
|
2019-04-02 05:37:03 +00:00
|
|
|
|
The --pagesize option or the --imgsize option with the --border option will
|
|
|
|
|
determine the MediaBox size of the resulting PDF document.
|
2018-12-25 11:30:10 +00:00
|
|
|
|
"""
|
|
|
|
|
% default_dpi,
|
|
|
|
|
)
|
2015-05-10 12:35:09 +00:00
|
|
|
|
|
|
|
|
|
sizeargs.add_argument(
|
2018-12-25 11:30:10 +00:00
|
|
|
|
"-S",
|
|
|
|
|
"--pagesize",
|
|
|
|
|
metavar="LxL",
|
|
|
|
|
type=parse_pagesize_rectarg,
|
|
|
|
|
help="""
|
2015-05-10 12:35:09 +00:00
|
|
|
|
Sets the size of the PDF pages. The short-option is the upper case S because
|
2018-12-25 11:30:10 +00:00
|
|
|
|
it is an mnemonic for being bigger than the image size.""",
|
|
|
|
|
)
|
2015-05-10 12:35:09 +00:00
|
|
|
|
|
|
|
|
|
sizeargs.add_argument(
|
2018-12-25 11:30:10 +00:00
|
|
|
|
"-s",
|
|
|
|
|
"--imgsize",
|
|
|
|
|
metavar="LxL",
|
|
|
|
|
type=parse_imgsize_rectarg,
|
|
|
|
|
help="""
|
2015-05-10 12:35:09 +00:00
|
|
|
|
Sets the size of the images on the PDF pages. In addition, the unit dpi is
|
|
|
|
|
allowed which will set the image size as a value of dots per inch. Instead of
|
|
|
|
|
a unit, width and height values may also have a percentage sign appended,
|
|
|
|
|
indicating a resize of the image by that percentage. The short-option is the
|
|
|
|
|
lower case s because it is an mnemonic for being smaller than the page size.
|
2018-12-25 11:30:10 +00:00
|
|
|
|
""",
|
|
|
|
|
)
|
2015-05-10 12:35:09 +00:00
|
|
|
|
sizeargs.add_argument(
|
2018-12-25 11:30:10 +00:00
|
|
|
|
"-b",
|
|
|
|
|
"--border",
|
|
|
|
|
metavar="L[:L]",
|
|
|
|
|
type=parse_borderarg,
|
|
|
|
|
help="""
|
2015-05-10 12:35:09 +00:00
|
|
|
|
Specifies the minimal distance between the image border and the PDF page
|
|
|
|
|
border. This value Is overwritten by explicit values set by --pagesize or
|
|
|
|
|
--imgsize. The value will be used when calculating page dimensions from the
|
|
|
|
|
image dimensions or the other way round. One, or two length values can be given
|
|
|
|
|
as an argument, separated by a colon. One value specifies the minimal border on
|
|
|
|
|
all four sides. Two values specify the minimal border on the top/bottom and
|
|
|
|
|
left/right, respectively. It is not possible to specify asymmetric borders
|
|
|
|
|
because images will always be centered on the page.
|
2018-12-25 11:30:10 +00:00
|
|
|
|
""",
|
|
|
|
|
)
|
2015-05-10 12:35:09 +00:00
|
|
|
|
sizeargs.add_argument(
|
2018-12-25 11:30:10 +00:00
|
|
|
|
"-f",
|
|
|
|
|
"--fit",
|
|
|
|
|
metavar="FIT",
|
|
|
|
|
type=parse_fitarg,
|
|
|
|
|
default=FitMode.into,
|
|
|
|
|
help="""
|
2015-05-10 12:35:09 +00:00
|
|
|
|
|
|
|
|
|
If --imgsize is given, fits the image using these dimensions. Otherwise, fit
|
|
|
|
|
the image into the dimensions given by --pagesize. FIT is one of into, fill,
|
|
|
|
|
exact, shrink and enlarge. The default value is "into". See the epilogue at the
|
|
|
|
|
bottom for a description of the FIT options.
|
|
|
|
|
|
2018-12-25 11:30:10 +00:00
|
|
|
|
""",
|
|
|
|
|
)
|
2015-05-10 12:35:09 +00:00
|
|
|
|
sizeargs.add_argument(
|
2018-12-25 11:30:10 +00:00
|
|
|
|
"-a",
|
|
|
|
|
"--auto-orient",
|
|
|
|
|
action="store_true",
|
|
|
|
|
help="""
|
2015-05-10 12:35:09 +00:00
|
|
|
|
If both dimensions of the page are given via --pagesize, conditionally swaps
|
|
|
|
|
these dimensions such that the page orientation is the same as the orientation
|
|
|
|
|
of the input image. If the orientation of a page gets flipped, then so do the
|
|
|
|
|
values set via the --border option.
|
2019-04-02 05:37:03 +00:00
|
|
|
|
""",
|
|
|
|
|
)
|
2021-05-14 09:30:23 +00:00
|
|
|
|
sizeargs.add_argument(
|
|
|
|
|
"-r",
|
|
|
|
|
"--rotation",
|
|
|
|
|
"--orientation",
|
|
|
|
|
metavar="ROT",
|
|
|
|
|
type=parse_rotationarg,
|
|
|
|
|
default=Rotation.auto,
|
|
|
|
|
help="""
|
|
|
|
|
Specifies how input images should be rotated. ROT can be one of auto, none,
|
|
|
|
|
ifvalid, 0, 90, 180 and 270. The default value is auto and indicates that input
|
|
|
|
|
images are rotated according to their EXIF Orientation tag. The values none and
|
|
|
|
|
0 ignore the EXIF Orientation values of the input images. The value ifvalid
|
|
|
|
|
acts like auto but ignores invalid EXIF rotation values and only issues a
|
|
|
|
|
warning instead of throwing an error. The values 90, 180 and 270 perform a
|
|
|
|
|
clockwise rotation of the image.
|
|
|
|
|
""",
|
|
|
|
|
)
|
2019-04-02 05:37:03 +00:00
|
|
|
|
sizeargs.add_argument(
|
|
|
|
|
"--crop-border",
|
|
|
|
|
metavar="L[:L]",
|
|
|
|
|
type=parse_borderarg,
|
|
|
|
|
help="""
|
|
|
|
|
Specifies the border between the CropBox and the MediaBox. One, or two length
|
|
|
|
|
values can be given as an argument, separated by a colon. One value specifies
|
|
|
|
|
the border on all four sides. Two values specify the border on the top/bottom
|
|
|
|
|
and left/right, respectively. It is not possible to specify asymmetric borders.
|
|
|
|
|
""",
|
|
|
|
|
)
|
|
|
|
|
sizeargs.add_argument(
|
|
|
|
|
"--bleed-border",
|
|
|
|
|
metavar="L[:L]",
|
|
|
|
|
type=parse_borderarg,
|
|
|
|
|
help="""
|
|
|
|
|
Specifies the border between the BleedBox and the MediaBox. One, or two length
|
|
|
|
|
values can be given as an argument, separated by a colon. One value specifies
|
|
|
|
|
the border on all four sides. Two values specify the border on the top/bottom
|
|
|
|
|
and left/right, respectively. It is not possible to specify asymmetric borders.
|
|
|
|
|
""",
|
|
|
|
|
)
|
|
|
|
|
sizeargs.add_argument(
|
|
|
|
|
"--trim-border",
|
|
|
|
|
metavar="L[:L]",
|
|
|
|
|
type=parse_borderarg,
|
|
|
|
|
help="""
|
|
|
|
|
Specifies the border between the TrimBox and the MediaBox. One, or two length
|
|
|
|
|
values can be given as an argument, separated by a colon. One value specifies
|
|
|
|
|
the border on all four sides. Two values specify the border on the top/bottom
|
|
|
|
|
and left/right, respectively. It is not possible to specify asymmetric borders.
|
|
|
|
|
""",
|
|
|
|
|
)
|
|
|
|
|
sizeargs.add_argument(
|
|
|
|
|
"--art-border",
|
|
|
|
|
metavar="L[:L]",
|
|
|
|
|
type=parse_borderarg,
|
|
|
|
|
help="""
|
|
|
|
|
Specifies the border between the ArtBox and the MediaBox. One, or two length
|
|
|
|
|
values can be given as an argument, separated by a colon. One value specifies
|
|
|
|
|
the border on all four sides. Two values specify the border on the top/bottom
|
|
|
|
|
and left/right, respectively. It is not possible to specify asymmetric borders.
|
2018-12-25 11:30:10 +00:00
|
|
|
|
""",
|
|
|
|
|
)
|
2015-05-10 12:35:09 +00:00
|
|
|
|
|
2017-01-20 04:17:23 +00:00
|
|
|
|
metaargs = parser.add_argument_group(
|
2018-12-25 11:30:10 +00:00
|
|
|
|
title="Arguments setting metadata",
|
|
|
|
|
description="Options handling embedded timestamps, title and author "
|
|
|
|
|
"information.",
|
|
|
|
|
)
|
2015-05-10 12:35:09 +00:00
|
|
|
|
metaargs.add_argument(
|
2018-12-25 11:30:10 +00:00
|
|
|
|
"--title", metavar="title", type=str, help="Sets the title metadata value"
|
|
|
|
|
)
|
2015-05-10 12:35:09 +00:00
|
|
|
|
metaargs.add_argument(
|
2018-12-25 11:30:10 +00:00
|
|
|
|
"--author", metavar="author", type=str, help="Sets the author metadata value"
|
|
|
|
|
)
|
2015-05-10 12:35:09 +00:00
|
|
|
|
metaargs.add_argument(
|
2018-12-25 11:30:10 +00:00
|
|
|
|
"--creator", metavar="creator", type=str, help="Sets the creator metadata value"
|
|
|
|
|
)
|
2015-05-10 12:35:09 +00:00
|
|
|
|
metaargs.add_argument(
|
2018-12-25 11:30:10 +00:00
|
|
|
|
"--producer",
|
|
|
|
|
metavar="producer",
|
|
|
|
|
type=str,
|
2016-02-12 12:55:58 +00:00
|
|
|
|
default="img2pdf " + __version__,
|
2018-12-25 11:30:10 +00:00
|
|
|
|
help="Sets the producer metadata value "
|
|
|
|
|
"(default is: img2pdf " + __version__ + ")",
|
|
|
|
|
)
|
2015-05-10 12:35:09 +00:00
|
|
|
|
metaargs.add_argument(
|
2018-12-25 11:30:10 +00:00
|
|
|
|
"--creationdate",
|
|
|
|
|
metavar="creationdate",
|
|
|
|
|
type=valid_date,
|
|
|
|
|
help="Sets the UTC creation date metadata value in YYYY-MM-DD or "
|
|
|
|
|
"YYYY-MM-DDTHH:MM or YYYY-MM-DDTHH:MM:SS format or any format "
|
|
|
|
|
"understood by python dateutil module or any format understood "
|
|
|
|
|
"by `date --date`",
|
|
|
|
|
)
|
2015-05-10 12:35:09 +00:00
|
|
|
|
metaargs.add_argument(
|
2018-12-25 11:30:10 +00:00
|
|
|
|
"--moddate",
|
|
|
|
|
metavar="moddate",
|
|
|
|
|
type=valid_date,
|
|
|
|
|
help="Sets the UTC modification date metadata value in YYYY-MM-DD "
|
|
|
|
|
"or YYYY-MM-DDTHH:MM or YYYY-MM-DDTHH:MM:SS format or any format "
|
|
|
|
|
"understood by python dateutil module or any format understood "
|
|
|
|
|
"by `date --date`",
|
|
|
|
|
)
|
2015-05-10 12:35:09 +00:00
|
|
|
|
metaargs.add_argument(
|
2018-12-25 11:30:10 +00:00
|
|
|
|
"--subject", metavar="subject", type=str, help="Sets the subject metadata value"
|
|
|
|
|
)
|
2015-05-10 12:35:09 +00:00
|
|
|
|
metaargs.add_argument(
|
2018-12-25 11:30:10 +00:00
|
|
|
|
"--keywords",
|
|
|
|
|
metavar="kw",
|
|
|
|
|
type=str,
|
|
|
|
|
nargs="+",
|
|
|
|
|
help="Sets the keywords metadata value (can be given multiple times)",
|
|
|
|
|
)
|
2015-05-10 12:35:09 +00:00
|
|
|
|
|
|
|
|
|
viewerargs = parser.add_argument_group(
|
2018-12-25 11:30:10 +00:00
|
|
|
|
title="PDF viewer arguments",
|
|
|
|
|
description="PDF files can specify how they are meant to be "
|
|
|
|
|
"presented to the user by a PDF viewer",
|
|
|
|
|
)
|
2015-05-10 12:35:09 +00:00
|
|
|
|
|
|
|
|
|
viewerargs.add_argument(
|
2018-12-25 11:30:10 +00:00
|
|
|
|
"--viewer-panes",
|
|
|
|
|
metavar="PANES",
|
|
|
|
|
type=parse_panes,
|
|
|
|
|
help="Instruct the PDF viewer which side panes to show. Valid values "
|
|
|
|
|
'are "outlines" and "thumbs". It is not possible to specify both '
|
|
|
|
|
"at the same time.",
|
|
|
|
|
)
|
2015-05-10 12:35:09 +00:00
|
|
|
|
viewerargs.add_argument(
|
2018-12-25 11:30:10 +00:00
|
|
|
|
"--viewer-initial-page",
|
|
|
|
|
metavar="NUM",
|
|
|
|
|
type=int,
|
|
|
|
|
help="Instead of showing the first page, instruct the PDF viewer to "
|
|
|
|
|
"show the given page instead. Page numbers start with 1.",
|
|
|
|
|
)
|
2015-05-10 12:35:09 +00:00
|
|
|
|
viewerargs.add_argument(
|
2018-12-25 11:30:10 +00:00
|
|
|
|
"--viewer-magnification",
|
|
|
|
|
metavar="MAG",
|
|
|
|
|
type=parse_magnification,
|
|
|
|
|
help="Instruct the PDF viewer to open the PDF with a certain zoom "
|
|
|
|
|
"level. Valid values are either a floating point number giving "
|
|
|
|
|
'the exact zoom level, "fit" (zoom to fit whole page), "fith" '
|
|
|
|
|
'(zoom to fit page width) and "fitbh" (zoom to fit visible page '
|
|
|
|
|
"width).",
|
|
|
|
|
)
|
2015-05-10 12:35:09 +00:00
|
|
|
|
viewerargs.add_argument(
|
2018-12-25 11:30:10 +00:00
|
|
|
|
"--viewer-page-layout",
|
|
|
|
|
metavar="LAYOUT",
|
|
|
|
|
type=parse_layout,
|
|
|
|
|
help="Instruct the PDF viewer how to arrange the pages on the screen. "
|
|
|
|
|
'Valid values are "single" (display single pages), "onecolumn" '
|
|
|
|
|
'(one continuous column), "twocolumnright" (two continuous '
|
|
|
|
|
'columns with odd number pages on the right) and "twocolumnleft" '
|
|
|
|
|
"(two continuous columns with odd numbered pages on the left)",
|
|
|
|
|
)
|
2015-05-10 12:35:09 +00:00
|
|
|
|
viewerargs.add_argument(
|
2018-12-25 11:30:10 +00:00
|
|
|
|
"--viewer-fit-window",
|
|
|
|
|
action="store_true",
|
2019-03-28 23:34:31 +00:00
|
|
|
|
help="Instruct the PDF viewer to resize the window to fit the page size",
|
2018-12-25 11:30:10 +00:00
|
|
|
|
)
|
2015-05-10 12:35:09 +00:00
|
|
|
|
viewerargs.add_argument(
|
2018-12-25 11:30:10 +00:00
|
|
|
|
"--viewer-center-window",
|
|
|
|
|
action="store_true",
|
|
|
|
|
help="Instruct the PDF viewer to center the PDF viewer window",
|
|
|
|
|
)
|
2015-05-10 12:35:09 +00:00
|
|
|
|
viewerargs.add_argument(
|
2018-12-25 11:30:10 +00:00
|
|
|
|
"--viewer-fullscreen",
|
|
|
|
|
action="store_true",
|
|
|
|
|
help="Instruct the PDF viewer to open the PDF in fullscreen mode",
|
|
|
|
|
)
|
2015-05-10 12:35:09 +00:00
|
|
|
|
|
2017-08-07 09:21:42 +00:00
|
|
|
|
args = parser.parse_args(argv[1:])
|
2015-05-10 12:35:09 +00:00
|
|
|
|
|
|
|
|
|
if args.verbose:
|
|
|
|
|
logging.basicConfig(level=logging.DEBUG)
|
|
|
|
|
|
2018-03-24 18:47:03 +00:00
|
|
|
|
if args.pillow_limit_break:
|
|
|
|
|
Image.MAX_IMAGE_PIXELS = None
|
|
|
|
|
|
2019-08-25 04:03:48 +00:00
|
|
|
|
if args.gui:
|
|
|
|
|
gui()
|
|
|
|
|
sys.exit(0)
|
|
|
|
|
|
2020-04-05 10:21:09 +00:00
|
|
|
|
layout_fun = get_layout_fun(
|
|
|
|
|
args.pagesize, args.imgsize, args.border, args.fit, args.auto_orient
|
|
|
|
|
)
|
|
|
|
|
|
2021-05-07 08:58:46 +00:00
|
|
|
|
if len(args.images) > 0 and len(args.from_file) > 0:
|
|
|
|
|
logger.error(
|
|
|
|
|
"%s: error: cannot use --from-file with positional arguments" % parser.prog
|
|
|
|
|
)
|
|
|
|
|
sys.exit(2)
|
|
|
|
|
elif len(args.images) == 0 and len(args.from_file) == 0:
|
|
|
|
|
# if no positional arguments were supplied, read a single image from
|
|
|
|
|
# standard input
|
2021-04-12 12:12:14 +00:00
|
|
|
|
logger.info("reading image from standard input")
|
2015-05-10 12:35:09 +00:00
|
|
|
|
try:
|
2021-05-07 08:58:46 +00:00
|
|
|
|
images = [sys.stdin.buffer.read()]
|
2015-05-10 12:35:09 +00:00
|
|
|
|
except KeyboardInterrupt:
|
2021-04-12 04:52:27 +00:00
|
|
|
|
sys.exit(0)
|
2021-05-07 08:58:46 +00:00
|
|
|
|
elif len(args.images) > 0 and len(args.from_file) == 0:
|
|
|
|
|
# On windows, each positional argument can expand into multiple paths
|
|
|
|
|
# because we do globbing ourselves. Here we flatten the list of lists
|
|
|
|
|
# again.
|
|
|
|
|
images = chain.from_iterable(args.images)
|
|
|
|
|
elif len(args.images) == 0 and len(args.from_file) > 0:
|
|
|
|
|
images = args.from_file
|
2015-05-10 12:35:09 +00:00
|
|
|
|
|
|
|
|
|
# with the number of pages being equal to the number of images, the
|
|
|
|
|
# value passed to --viewer-initial-page must be between 1 and that number
|
|
|
|
|
if args.viewer_initial_page is not None:
|
|
|
|
|
if args.viewer_initial_page < 1:
|
|
|
|
|
parser.print_usage(file=sys.stderr)
|
2021-04-12 12:12:14 +00:00
|
|
|
|
logger.error(
|
2018-12-25 11:30:10 +00:00
|
|
|
|
"%s: error: argument --viewer-initial-page: must be "
|
|
|
|
|
"greater than zero" % parser.prog
|
|
|
|
|
)
|
2021-04-12 04:52:27 +00:00
|
|
|
|
sys.exit(2)
|
2021-05-07 08:58:46 +00:00
|
|
|
|
if args.viewer_initial_page > len(images):
|
2015-05-10 12:35:09 +00:00
|
|
|
|
parser.print_usage(file=sys.stderr)
|
2021-04-12 12:12:14 +00:00
|
|
|
|
logger.error(
|
2018-12-25 11:30:10 +00:00
|
|
|
|
"%s: error: argument --viewer-initial-page: must be "
|
|
|
|
|
"less than or equal to the total number of pages" % parser.prog
|
|
|
|
|
)
|
2021-04-12 04:52:27 +00:00
|
|
|
|
sys.exit(2)
|
2014-08-04 15:25:07 +00:00
|
|
|
|
|
2016-02-13 08:31:33 +00:00
|
|
|
|
try:
|
|
|
|
|
convert(
|
2021-05-07 08:58:46 +00:00
|
|
|
|
*images,
|
2020-05-28 10:46:37 +00:00
|
|
|
|
engine=args.engine,
|
2018-12-25 11:30:10 +00:00
|
|
|
|
title=args.title,
|
|
|
|
|
author=args.author,
|
|
|
|
|
creator=args.creator,
|
|
|
|
|
producer=args.producer,
|
|
|
|
|
creationdate=args.creationdate,
|
|
|
|
|
moddate=args.moddate,
|
|
|
|
|
subject=args.subject,
|
|
|
|
|
keywords=args.keywords,
|
|
|
|
|
colorspace=args.colorspace,
|
|
|
|
|
nodate=args.nodate,
|
|
|
|
|
layout_fun=layout_fun,
|
|
|
|
|
viewer_panes=args.viewer_panes,
|
2016-02-13 08:31:33 +00:00
|
|
|
|
viewer_initial_page=args.viewer_initial_page,
|
|
|
|
|
viewer_magnification=args.viewer_magnification,
|
|
|
|
|
viewer_page_layout=args.viewer_page_layout,
|
|
|
|
|
viewer_fit_window=args.viewer_fit_window,
|
|
|
|
|
viewer_center_window=args.viewer_center_window,
|
2018-12-25 11:30:10 +00:00
|
|
|
|
viewer_fullscreen=args.viewer_fullscreen,
|
|
|
|
|
outputstream=args.output,
|
2019-04-02 05:37:03 +00:00
|
|
|
|
first_frame_only=args.first_frame_only,
|
|
|
|
|
cropborder=args.crop_border,
|
|
|
|
|
bleedborder=args.bleed_border,
|
|
|
|
|
trimborder=args.trim_border,
|
|
|
|
|
artborder=args.art_border,
|
2020-06-01 00:10:07 +00:00
|
|
|
|
pdfa=args.pdfa,
|
2021-05-14 09:30:23 +00:00
|
|
|
|
rotation=args.rotation,
|
2018-12-25 11:30:10 +00:00
|
|
|
|
)
|
2016-02-13 08:31:33 +00:00
|
|
|
|
except Exception as e:
|
2021-04-12 12:12:14 +00:00
|
|
|
|
logger.error("error: " + str(e))
|
|
|
|
|
if logger.isEnabledFor(logging.DEBUG):
|
2016-02-14 17:55:27 +00:00
|
|
|
|
import traceback
|
2018-12-25 11:30:10 +00:00
|
|
|
|
|
2016-02-14 17:55:27 +00:00
|
|
|
|
traceback.print_exc(file=sys.stderr)
|
2021-04-12 04:52:27 +00:00
|
|
|
|
sys.exit(1)
|
2014-03-14 18:13:03 +00:00
|
|
|
|
|
2017-01-19 10:22:08 +00:00
|
|
|
|
|
2018-12-25 11:30:10 +00:00
|
|
|
|
if __name__ == "__main__":
|
2014-03-14 18:13:03 +00:00
|
|
|
|
main()
|