forked from josch/img2pdf
Add support for JBIG2 (generic coding)
Implements the proposal detailed at josch/img2pdf#112 (comment) This is a limited implementation of JBIG2, which can be extended to support multiple pages, symbol tables, and other features of the format in the future. To test, I included a test fixture. You can also download 042.bmp (the same one as @josch already downloaded in josch/img2pdf#112 (comment) from https://git.ghostscript.com/?p=tests.git;a=blob_plain;f=jbig2/042.bmp;hb=HEAD and run the following command: jbig2 042.bmp | img2pdf > 042.pdf This results in a small PDF, just as @josch originally found in the comment mentioned above. This is my first contribution to this repository so let me know if something else is needed. Thanks for a great library!
This commit is contained in:
parent
819b366bf5
commit
085dd192f6
6 changed files with 96 additions and 11 deletions
|
@ -33,12 +33,14 @@ input file format and image color space.
|
||||||
| JPEG2000 | any | direct |
|
| JPEG2000 | any | direct |
|
||||||
| PNG (non-interlaced, no transparency) | any | direct |
|
| PNG (non-interlaced, no transparency) | any | direct |
|
||||||
| TIFF (CCITT Group 4) | monochrome | direct |
|
| TIFF (CCITT Group 4) | monochrome | direct |
|
||||||
|
| JBIG2 (single-page generic coding) | bi-level | direct |
|
||||||
| any | any except CMYK and monochrome | PNG Paeth |
|
| any | any except CMYK and monochrome | PNG Paeth |
|
||||||
| any | monochrome | CCITT Group 4 |
|
| any | monochrome | CCITT Group 4 |
|
||||||
| any | CMYK | flate |
|
| any | CMYK | flate |
|
||||||
|
|
||||||
For JPEG, JPEG2000, non-interlaced PNG and TIFF images with CCITT Group 4
|
For JPEG, JPEG2000, non-interlaced PNG, TIFF images with CCITT Group 4
|
||||||
encoded data, img2pdf directly embeds the image data into the PDF without
|
encoded data, and JBIG2 with single-page generic coding (e.g. using `jbig2enc`),
|
||||||
|
img2pdf directly embeds the image data into the PDF without
|
||||||
re-encoding it. It thus treats the PDF format merely as a container format for
|
re-encoding it. It thus treats the PDF format merely as a container format for
|
||||||
the image data. In these cases, img2pdf only increases the filesize by the size
|
the image data. In these cases, img2pdf only increases the filesize by the size
|
||||||
of the PDF container (typically around 500 to 700 bytes). Since data is only
|
of the PDF container (typically around 500 to 700 bytes). Since data is only
|
||||||
|
|
|
@ -128,7 +128,7 @@ PageOrientation = Enum("PageOrientation", "portrait landscape")
|
||||||
Colorspace = Enum("Colorspace", "RGB RGBA L LA 1 CMYK CMYK;I P PA other")
|
Colorspace = Enum("Colorspace", "RGB RGBA L LA 1 CMYK CMYK;I P PA other")
|
||||||
|
|
||||||
ImageFormat = Enum(
|
ImageFormat = Enum(
|
||||||
"ImageFormat", "JPEG JPEG2000 CCITTGroup4 PNG GIF TIFF MPO MIFF other"
|
"ImageFormat", "JPEG JPEG2000 CCITTGroup4 PNG GIF TIFF MPO MIFF JBIG2 other"
|
||||||
)
|
)
|
||||||
|
|
||||||
PageMode = Enum("PageMode", "none outlines thumbs")
|
PageMode = Enum("PageMode", "none outlines thumbs")
|
||||||
|
@ -918,6 +918,11 @@ class pdfdoc(object):
|
||||||
self.output_version = "1.5" # jpeg2000 needs pdf 1.5
|
self.output_version = "1.5" # jpeg2000 needs pdf 1.5
|
||||||
elif imgformat is ImageFormat.CCITTGroup4:
|
elif imgformat is ImageFormat.CCITTGroup4:
|
||||||
ofilter = [PdfName.CCITTFaxDecode]
|
ofilter = [PdfName.CCITTFaxDecode]
|
||||||
|
elif imgformat is ImageFormat.JBIG2:
|
||||||
|
ofilter = PdfName.JBIG2Decode
|
||||||
|
# JBIG2Decode requires PDF 1.4
|
||||||
|
if self.output_version < "1.4":
|
||||||
|
self.output_version = "1.4"
|
||||||
else:
|
else:
|
||||||
ofilter = PdfName.FlateDecode
|
ofilter = PdfName.FlateDecode
|
||||||
|
|
||||||
|
@ -1308,6 +1313,19 @@ def get_imgmetadata(
|
||||||
if vdpi is None:
|
if vdpi is None:
|
||||||
vdpi = default_dpi
|
vdpi = default_dpi
|
||||||
ndpi = (hdpi, vdpi)
|
ndpi = (hdpi, vdpi)
|
||||||
|
elif imgformat == ImageFormat.JBIG2:
|
||||||
|
imgwidthpx, imgheightpx, xres, yres = struct.unpack('>IIII', rawdata[24:40])
|
||||||
|
INCH_PER_METER = 39.370079
|
||||||
|
if xres == 0:
|
||||||
|
hdpi = default_dpi
|
||||||
|
else:
|
||||||
|
hdpi = int(float(xres) / INCH_PER_METER)
|
||||||
|
if yres == 0:
|
||||||
|
vdpi = default_dpi
|
||||||
|
else:
|
||||||
|
vdpi = int(float(yres) / INCH_PER_METER)
|
||||||
|
ndpi = (hdpi, vdpi)
|
||||||
|
ics = "1"
|
||||||
else:
|
else:
|
||||||
imgwidthpx, imgheightpx = imgdata.size
|
imgwidthpx, imgheightpx = imgdata.size
|
||||||
|
|
||||||
|
@ -1334,7 +1352,7 @@ def get_imgmetadata(
|
||||||
|
|
||||||
# GIF and PNG files with transparency are supported
|
# GIF and PNG files with transparency are supported
|
||||||
if imgformat in [ImageFormat.PNG, ImageFormat.GIF, ImageFormat.JPEG2000] and (
|
if imgformat in [ImageFormat.PNG, ImageFormat.GIF, ImageFormat.JPEG2000] and (
|
||||||
ics in ["RGBA", "LA"] or "transparency" in imgdata.info
|
ics in ["RGBA", "LA"] or (imgdata is not None and "transparency" in imgdata.info)
|
||||||
):
|
):
|
||||||
# Must check the IHDR chunk for the bit depth, because PIL would lossily
|
# Must check the IHDR chunk for the bit depth, because PIL would lossily
|
||||||
# convert 16-bit RGBA/LA images to 8-bit.
|
# convert 16-bit RGBA/LA images to 8-bit.
|
||||||
|
@ -1350,7 +1368,7 @@ def get_imgmetadata(
|
||||||
raise AlphaChannelError(
|
raise AlphaChannelError(
|
||||||
"Refusing to work with multiple >8bit channels."
|
"Refusing to work with multiple >8bit channels."
|
||||||
)
|
)
|
||||||
elif ics in ["LA", "PA", "RGBA"] or "transparency" in imgdata.info:
|
elif ics in ["LA", "PA", "RGBA"] or (imgdata is not None and "transparency" in imgdata.info):
|
||||||
raise AlphaChannelError("This function must not be called on images with alpha")
|
raise AlphaChannelError("This function must not be called on images with alpha")
|
||||||
|
|
||||||
# Since commit 07a96209597c5e8dfe785c757d7051ce67a980fb or release 4.1.0
|
# Since commit 07a96209597c5e8dfe785c757d7051ce67a980fb or release 4.1.0
|
||||||
|
@ -1455,7 +1473,7 @@ def get_imgmetadata(
|
||||||
logger.debug("input colorspace = %s", color.name)
|
logger.debug("input colorspace = %s", color.name)
|
||||||
|
|
||||||
iccp = None
|
iccp = None
|
||||||
if "icc_profile" in imgdata.info:
|
if imgdata is not None and "icc_profile" in imgdata.info:
|
||||||
iccp = imgdata.info.get("icc_profile")
|
iccp = imgdata.info.get("icc_profile")
|
||||||
# GIMP saves bilevel TIFF images and palette PNG images with only black and
|
# GIMP saves bilevel TIFF images and palette PNG images with only black and
|
||||||
# white in the palette with an RGB ICC profile which is useless
|
# white in the palette with an RGB ICC profile which is useless
|
||||||
|
@ -1805,8 +1823,6 @@ def parse_miff(data):
|
||||||
results.extend(parse_miff(rest[lenpal + lenimgdata :]))
|
results.extend(parse_miff(rest[lenpal + lenimgdata :]))
|
||||||
return results
|
return results
|
||||||
# fmt: on
|
# fmt: on
|
||||||
|
|
||||||
|
|
||||||
def read_images(
|
def read_images(
|
||||||
rawdata, colorspace, first_frame_only=False, rot=None, include_thumbnails=False
|
rawdata, colorspace, first_frame_only=False, rot=None, include_thumbnails=False
|
||||||
):
|
):
|
||||||
|
@ -1820,7 +1836,42 @@ def read_images(
|
||||||
if rawdata[:12] == b"\x00\x00\x00\x0C\x6A\x50\x20\x20\x0D\x0A\x87\x0A":
|
if rawdata[:12] == b"\x00\x00\x00\x0C\x6A\x50\x20\x20\x0D\x0A\x87\x0A":
|
||||||
# image is jpeg2000
|
# image is jpeg2000
|
||||||
imgformat = ImageFormat.JPEG2000
|
imgformat = ImageFormat.JPEG2000
|
||||||
if rawdata[:14].lower() == b"id=imagemagick":
|
elif rawdata[:8] == b"\x97\x4a\x42\x32\x0d\x0a\x1a\x0a":
|
||||||
|
# For now we only support single-page generic coding of JBIG2, for example as generated by
|
||||||
|
# https://github.com/agl/jbig2enc
|
||||||
|
#
|
||||||
|
# In fact, you can pipe an example image like 042.bmp from https://git.ghostscript.com/?p=tests.git;a=blob_plain;f=jbig2/042.bmp;hb=HEAD
|
||||||
|
# directly into img2pdf:
|
||||||
|
# jbig2 042.bmp | img2pdf > 042.pdf
|
||||||
|
#
|
||||||
|
# For this we assume that the first 13 bytes are the JBIG file header describing a document with one page,
|
||||||
|
# followed by a "page information" segment describing the dimensions of that page.
|
||||||
|
#
|
||||||
|
# The following annotated `hexdump -C 042.jb2` shows the first 40 bytes that we inspect directly.
|
||||||
|
# The first 24 bytes (until "||") have to match exactly, while the following 16 bytes are read by get_imgmetadata.
|
||||||
|
#
|
||||||
|
# 97 4a 42 32 0d 0a 1a 0a 01 00 00 00 01 00 00 00
|
||||||
|
# \_____________________/ | \_________/ \______
|
||||||
|
# magic-bytes org/unk pages seg-num
|
||||||
|
#
|
||||||
|
# 00 30 00 01 00 00 00 13 || 00 00 06 c0 00 00 09 23
|
||||||
|
# _/ | | | \_________/ || \_________/ \_________/
|
||||||
|
# type refs page seg-size || width-px height-px
|
||||||
|
#
|
||||||
|
# 00 00 00 00 00 00 00 00
|
||||||
|
# \_________/ \_________/
|
||||||
|
# xres yres
|
||||||
|
#
|
||||||
|
# For more information on the data format, see:
|
||||||
|
# * https://github.com/agl/jbig2enc/blob/ea05019/fcd14492.pdf
|
||||||
|
# For more information about the generic coding, see:
|
||||||
|
# * https://github.com/agl/jbig2enc/blob/ea05019/src/jbig2enc.cc#L898
|
||||||
|
imgformat = ImageFormat.JBIG2
|
||||||
|
if rawdata[:24] != b"\x97\x4a\x42\x32\x0d\x0a\x1a\x0a\x01\x00\x00\x00\x01\x00\x00\x00\x00\x30\x00\x01\x00\x00\x00\x13":
|
||||||
|
raise ImageOpenError(
|
||||||
|
"Unsupported JBIG2 format; only single-page generic coding is supported (e.g. from `jbig2enc`)"
|
||||||
|
)
|
||||||
|
elif rawdata[:14].lower() == b"id=imagemagick":
|
||||||
# image is in MIFF format
|
# image is in MIFF format
|
||||||
# this is useful for 16 bit CMYK because PNG cannot do CMYK and thus
|
# this is useful for 16 bit CMYK because PNG cannot do CMYK and thus
|
||||||
# we need PIL but PIL cannot do 16 bit
|
# we need PIL but PIL cannot do 16 bit
|
||||||
|
@ -2066,6 +2117,28 @@ def read_images(
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if imgformat == ImageFormat.JBIG2:
|
||||||
|
color, ndpi, imgwidthpx, imgheightpx, rotation, iccp = get_imgmetadata(
|
||||||
|
imgdata, imgformat, default_dpi, colorspace, rawdata, rot
|
||||||
|
)
|
||||||
|
streamdata = rawdata[13:] # Strip file header
|
||||||
|
return [
|
||||||
|
(
|
||||||
|
color,
|
||||||
|
ndpi,
|
||||||
|
imgformat,
|
||||||
|
streamdata,
|
||||||
|
None,
|
||||||
|
imgwidthpx,
|
||||||
|
imgheightpx,
|
||||||
|
[],
|
||||||
|
False,
|
||||||
|
1,
|
||||||
|
rotation,
|
||||||
|
iccp,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
if imgformat == ImageFormat.MIFF:
|
if imgformat == ImageFormat.MIFF:
|
||||||
return parse_miff(rawdata)
|
return parse_miff(rawdata)
|
||||||
|
|
||||||
|
|
|
@ -6861,7 +6861,7 @@ def test_layout(layout_test_cases):
|
||||||
|
|
||||||
@pytest.fixture(
|
@pytest.fixture(
|
||||||
scope="session",
|
scope="session",
|
||||||
params=os.listdir(os.path.join(os.path.dirname(__file__), "tests", "input")),
|
params=[filename for filename in os.listdir(os.path.join(os.path.dirname(__file__), "tests", "input")) if not filename.endswith(".jb2.bmp")],
|
||||||
)
|
)
|
||||||
def general_input(request):
|
def general_input(request):
|
||||||
assert os.path.isfile(
|
assert os.path.isfile(
|
||||||
|
@ -6987,6 +6987,11 @@ def test_general(general_input, engine):
|
||||||
assert x.Root.Type == "/Catalog"
|
assert x.Root.Type == "/Catalog"
|
||||||
assert sorted(x.Root.Pages.keys()) == ["/Count", "/Kids", "/Type"]
|
assert sorted(x.Root.Pages.keys()) == ["/Count", "/Kids", "/Type"]
|
||||||
assert x.Root.Pages.Type == "/Pages"
|
assert x.Root.Pages.Type == "/Pages"
|
||||||
|
if f.endswith(".jb2"):
|
||||||
|
# PIL doens't support .jb2, so we load the original .bmp, which
|
||||||
|
# was converted to the .jb2 using `jbig2enc`.
|
||||||
|
orig_img = Image.open(f.replace(".jb2", ".jb2.bmp"))
|
||||||
|
else:
|
||||||
orig_img = Image.open(f)
|
orig_img = Image.open(f)
|
||||||
for pagenum in range(len(x.Root.Pages.Kids)):
|
for pagenum in range(len(x.Root.Pages.Kids)):
|
||||||
# retrieve the original image frame that this page was
|
# retrieve the original image frame that this page was
|
||||||
|
@ -6995,6 +7000,8 @@ def test_general(general_input, engine):
|
||||||
cur_page = x.Root.Pages.Kids[pagenum]
|
cur_page = x.Root.Pages.Kids[pagenum]
|
||||||
|
|
||||||
ndpi = orig_img.info.get("dpi", (96.0, 96.0))
|
ndpi = orig_img.info.get("dpi", (96.0, 96.0))
|
||||||
|
if ndpi[0] <= 0.001 or ndpi[1] <= 0.001:
|
||||||
|
ndpi = (96.0, 96.0)
|
||||||
# In python3, the returned dpi value for some tiff images will
|
# In python3, the returned dpi value for some tiff images will
|
||||||
# not be an integer but a float. To make the behaviour of
|
# not be an integer but a float. To make the behaviour of
|
||||||
# img2pdf the same between python2 and python3, we convert that
|
# img2pdf the same between python2 and python3, we convert that
|
||||||
|
@ -7044,6 +7051,7 @@ def test_general(general_input, engine):
|
||||||
"/JPXDecode",
|
"/JPXDecode",
|
||||||
"/FlateDecode",
|
"/FlateDecode",
|
||||||
pikepdf.Array([pikepdf.Name.CCITTFaxDecode]),
|
pikepdf.Array([pikepdf.Name.CCITTFaxDecode]),
|
||||||
|
"/JBIG2Decode",
|
||||||
]
|
]
|
||||||
|
|
||||||
# test if the image has correct size
|
# test if the image has correct size
|
||||||
|
@ -7053,6 +7061,8 @@ def test_general(general_input, engine):
|
||||||
# verbatim into the PDF
|
# verbatim into the PDF
|
||||||
if imgprops.Filter in ["/DCTDecode", "/JPXDecode"]:
|
if imgprops.Filter in ["/DCTDecode", "/JPXDecode"]:
|
||||||
assert cur_page.Resources.XObject.Im0.read_raw_bytes() == orig_imgdata
|
assert cur_page.Resources.XObject.Im0.read_raw_bytes() == orig_imgdata
|
||||||
|
elif imgprops.Filter == "/JBIG2Decode":
|
||||||
|
assert cur_page.Resources.XObject.Im0.read_raw_bytes() == orig_imgdata[13:] # Strip file header
|
||||||
elif imgprops.Filter == pikepdf.Array([pikepdf.Name.CCITTFaxDecode]):
|
elif imgprops.Filter == pikepdf.Array([pikepdf.Name.CCITTFaxDecode]):
|
||||||
tiff_header = tiff_header_for_ccitt(
|
tiff_header = tiff_header_for_ccitt(
|
||||||
int(imgprops.Width), int(imgprops.Height), int(imgprops.Length), 4
|
int(imgprops.Width), int(imgprops.Height), int(imgprops.Length), 4
|
||||||
|
|
BIN
src/tests/input/042.jb2
Normal file
BIN
src/tests/input/042.jb2
Normal file
Binary file not shown.
BIN
src/tests/input/042.jb2.bmp
Normal file
BIN
src/tests/input/042.jb2.bmp
Normal file
Binary file not shown.
After Width: | Height: | Size: 493 KiB |
BIN
src/tests/output/042.jb2.pdf
Normal file
BIN
src/tests/output/042.jb2.pdf
Normal file
Binary file not shown.
Loading…
Reference in a new issue