Compare commits
No commits in common. "049a559f3c7e61728b40e9a84ffe4aaace7f3f33" and "bbc327ba5a411a0c72c0bee054d1eba6364548a7" have entirely different histories.
049a559f3c
...
bbc327ba5a
12
README.md
12
README.md
@ -10,7 +10,7 @@ python -m pdf_segmented input.xcf output.pdf
|
|||||||
|
|
||||||
Takes as input a [GIMP](https://www.gimp.org/) XCF file with one layer per page (bottom layer = first page).
|
Takes as input a [GIMP](https://www.gimp.org/) XCF file with one layer per page (bottom layer = first page).
|
||||||
|
|
||||||
Pixels will be considered to be foreground if they are fully black (#000000) and all 8 of their adjacent pixels are also fully black or fully white. All remaining pixels will be considered to be background. This is most easily accomplished by selecting all colour graphics in GIMP, inverting the selection (Ctrl+I), then applying the [Threshold tool](https://docs.gimp.org/3.0/en/gimp-tool-threshold.html).
|
All black pixels (#000000) will be considered to be foreground, and all remaining pixels will be considered to be background. This is most easily accomplished by selecting all colour graphics in GIMP, inverting the selection (Ctrl+I), then applying the [Threshold tool](https://docs.gimp.org/3.0/en/gimp-tool-threshold.html).
|
||||||
|
|
||||||
The foreground will be compressed losslessly using [JBIG2](https://en.wikipedia.org/wiki/JBIG2). The background will be compressed lossily using [JPEG](https://en.wikipedia.org/wiki/JPEG). JPEG quality can be controlled using the `--jpeg-quality` option; the default is the Pillow default (75% at time of writing).
|
The foreground will be compressed losslessly using [JBIG2](https://en.wikipedia.org/wiki/JBIG2). The background will be compressed lossily using [JPEG](https://en.wikipedia.org/wiki/JPEG). JPEG quality can be controlled using the `--jpeg-quality` option; the default is the Pillow default (75% at time of writing).
|
||||||
|
|
||||||
@ -18,10 +18,10 @@ Additional compression algorithms are supported (JPEG 2000, PNG); see `--help` f
|
|||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
* [Python 3](https://www.python.org/) (tested using 3.13.7)
|
* [Python 3](https://www.python.org/) (tested using 3.13.3)
|
||||||
* [NumPy](https://numpy.org/) (tested using 2.3.2)
|
* [NumPy](https://numpy.org/) (tested using 2.2.5)
|
||||||
* [Pillow](https://pillow.readthedocs.io/en/stable/) (tested using 11.3.0)
|
* [Pillow](https://pillow.readthedocs.io/en/stable/) (tested using 11.2.1)
|
||||||
* [pikepdf](https://pikepdf.readthedocs.io/en/latest/) (tested using 9.10.2)
|
* [pikepdf](https://pikepdf.readthedocs.io/en/latest/) (tested using 9.7.0)
|
||||||
* [DjVuLibre](https://djvu.sourceforge.net/) (tested using 3.5.28) – for DjVu output
|
* [DjVuLibre](https://djvu.sourceforge.net/) (tested using 3.5.28) – for DjVu output
|
||||||
* [ImageMagick](https://imagemagick.org/) (tested using 7.1.2.3)
|
* [ImageMagick](https://imagemagick.org/) (tested using 7.1.1.47)
|
||||||
* [jbig2enc](https://github.com/agl/jbig2enc) (tested using 0.30) – for JBIG2
|
* [jbig2enc](https://github.com/agl/jbig2enc) (tested using 0.30) – for JBIG2
|
||||||
|
@ -43,8 +43,8 @@ class CompressionOptions:
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class CompressedPage:
|
class CompressedPage:
|
||||||
fg: Optional[CompressedLayer]
|
fg: CompressedLayer
|
||||||
bg: Optional[CompressedLayer]
|
bg: CompressedLayer
|
||||||
|
|
||||||
def compress_pages(
|
def compress_pages(
|
||||||
input_pages: InputPages,
|
input_pages: InputPages,
|
||||||
@ -104,9 +104,6 @@ def compress_layer(
|
|||||||
tempdir: str
|
tempdir: str
|
||||||
) -> CompressedLayer:
|
) -> CompressedLayer:
|
||||||
|
|
||||||
if layer is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Compress the given layer
|
# Compress the given layer
|
||||||
if compression == 'iw44':
|
if compression == 'iw44':
|
||||||
return iw44_compress_layer(layer=layer, dpi=input_pages.dpi, tempdir=tempdir)
|
return iw44_compress_layer(layer=layer, dpi=input_pages.dpi, tempdir=tempdir)
|
||||||
|
@ -42,19 +42,16 @@ def djvu_write_pages(
|
|||||||
# Combine foreground and background
|
# Combine foreground and background
|
||||||
_, page_djvu_file = tempfile.mkstemp(suffix='.djvu', dir=tempdir)
|
_, page_djvu_file = tempfile.mkstemp(suffix='.djvu', dir=tempdir)
|
||||||
|
|
||||||
|
# TODO: Handle case where empty background or foreground
|
||||||
args = ['djvumake', page_djvu_file, 'INFO={},{},{}'.format(input_pages.width, input_pages.height, round(input_pages.dpi))]
|
args = ['djvumake', page_djvu_file, 'INFO={},{},{}'.format(input_pages.width, input_pages.height, round(input_pages.dpi))]
|
||||||
if compressed_page.fg:
|
|
||||||
args.append('Sjbz={}'.format(compressed_page.fg.filename))
|
args.append('Sjbz={}'.format(compressed_page.fg.filename))
|
||||||
if compressed_page.bg:
|
|
||||||
args.append('BG44={}'.format(compressed_page.bg.filename))
|
args.append('BG44={}'.format(compressed_page.bg.filename))
|
||||||
subprocess.run(args, check=True, capture_output=True)
|
subprocess.run(args, check=True, capture_output=True)
|
||||||
|
|
||||||
djvu_page_files.append(page_djvu_file)
|
djvu_page_files.append(page_djvu_file)
|
||||||
finally:
|
finally:
|
||||||
# Clean up
|
# Clean up
|
||||||
if compressed_page.bg:
|
|
||||||
compressed_page.bg.cleanup()
|
compressed_page.bg.cleanup()
|
||||||
if compressed_page.fg:
|
|
||||||
compressed_page.fg.cleanup()
|
compressed_page.fg.cleanup()
|
||||||
|
|
||||||
# Combine pages
|
# Combine pages
|
||||||
|
@ -42,14 +42,13 @@ def pdf_write_pages(
|
|||||||
page = pdf.add_blank_page(page_size=(width_pt, height_pt))
|
page = pdf.add_blank_page(page_size=(width_pt, height_pt))
|
||||||
|
|
||||||
# Write each layer to the page
|
# Write each layer to the page
|
||||||
|
# TODO: Handle case where empty background or foreground
|
||||||
content_instructions = []
|
content_instructions = []
|
||||||
pdf_write_layer(input_pages=input_pages, pdf=pdf, page=page, layer=compressed_page.bg, is_foreground=False, content_instructions=content_instructions)
|
pdf_write_layer(input_pages=input_pages, pdf=pdf, page=page, layer=compressed_page.bg, is_foreground=False, content_instructions=content_instructions)
|
||||||
pdf_write_layer(input_pages=input_pages, pdf=pdf, page=page, layer=compressed_page.fg, is_foreground=True, content_instructions=content_instructions)
|
pdf_write_layer(input_pages=input_pages, pdf=pdf, page=page, layer=compressed_page.fg, is_foreground=True, content_instructions=content_instructions)
|
||||||
|
|
||||||
# Clean up
|
# Clean up
|
||||||
if compressed_page.bg:
|
|
||||||
compressed_page.bg.cleanup()
|
compressed_page.bg.cleanup()
|
||||||
if compressed_page.fg:
|
|
||||||
compressed_page.fg.cleanup()
|
compressed_page.fg.cleanup()
|
||||||
|
|
||||||
# Generate content stream
|
# Generate content stream
|
||||||
@ -74,9 +73,6 @@ def pdf_write_layer(
|
|||||||
content_instructions,
|
content_instructions,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
if layer is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Write the layer to PDF
|
# Write the layer to PDF
|
||||||
if isinstance(layer, JBIG2Layer):
|
if isinstance(layer, JBIG2Layer):
|
||||||
pdf_write_image(
|
pdf_write_image(
|
||||||
|
@ -20,12 +20,12 @@ import numpy
|
|||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Generator, List, Optional
|
from typing import Generator, List
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class SegmentedPage:
|
class SegmentedPage:
|
||||||
fg: Optional[Image]
|
fg: Image
|
||||||
bg: Optional[Image]
|
bg: Image
|
||||||
|
|
||||||
def segment_pages(input_pages: InputPages) -> Generator[SegmentedPage]:
|
def segment_pages(input_pages: InputPages) -> Generator[SegmentedPage]:
|
||||||
for page_num, input_page in enumerate(input_pages.pages):
|
for page_num, input_page in enumerate(input_pages.pages):
|
||||||
@ -39,50 +39,18 @@ def segment_page(input_page: Image) -> SegmentedPage:
|
|||||||
# Convert image to foreground/background
|
# Convert image to foreground/background
|
||||||
image_rgb = input_page.convert('RGB')
|
image_rgb = input_page.convert('RGB')
|
||||||
numpy_rgb = numpy.asarray(image_rgb)
|
numpy_rgb = numpy.asarray(image_rgb)
|
||||||
|
|
||||||
# Precompute black and white pixels
|
|
||||||
black_pixels = (numpy_rgb[:,:,0] == 0) & (numpy_rgb[:,:,1] == 0) & (numpy_rgb[:,:,2] == 0)
|
black_pixels = (numpy_rgb[:,:,0] == 0) & (numpy_rgb[:,:,1] == 0) & (numpy_rgb[:,:,2] == 0)
|
||||||
white_pixels = (numpy_rgb[:,:,0] == 255) & (numpy_rgb[:,:,1] == 255) & (numpy_rgb[:,:,2] == 255)
|
|
||||||
black_or_white = black_pixels | white_pixels
|
|
||||||
|
|
||||||
# Precompute pixels with all neighbours either black or white
|
# Foreground is only black
|
||||||
bw1 = numpy.roll(black_or_white, (1, 1), (0, 1))
|
|
||||||
bw1[0,:] = True
|
|
||||||
bw1[:,0] = True
|
|
||||||
bw2 = numpy.roll(black_or_white, (1, 0), (0, 1))
|
|
||||||
bw2[0,:] = True
|
|
||||||
bw3 = numpy.roll(black_or_white, (1, -1), (0, 1))
|
|
||||||
bw3[0,:] = True
|
|
||||||
bw3[:,-1] = True
|
|
||||||
bw4 = numpy.roll(black_or_white, (0, -1), (0, 1))
|
|
||||||
bw4[:,-1] = True
|
|
||||||
bw5 = numpy.roll(black_or_white, (-1, -1), (0, 1))
|
|
||||||
bw5[-1,:] = True
|
|
||||||
bw5[:,-1] = True
|
|
||||||
bw6 = numpy.roll(black_or_white, (-1, 0), (0, 1))
|
|
||||||
bw6[-1,:] = True
|
|
||||||
bw7 = numpy.roll(black_or_white, (-1, 1), (0, 1))
|
|
||||||
bw7[-1,:] = True
|
|
||||||
bw7[:,0] = True
|
|
||||||
bw8 = numpy.roll(black_or_white, (0, 1), (0, 1))
|
|
||||||
bw8[:,0] = True
|
|
||||||
bw_neighbours = bw1 & bw2 & bw3 & bw4 & bw5 & bw6 & bw7 & bw8
|
|
||||||
|
|
||||||
# Foreground is only black pixels with all neighbours either black or white
|
|
||||||
fg_pixels = black_pixels & bw_neighbours
|
|
||||||
|
|
||||||
# Foreground - white out all non-foreground pixels
|
|
||||||
numpy_fg = numpy_rgb.copy()
|
numpy_fg = numpy_rgb.copy()
|
||||||
numpy_fg[~fg_pixels,:] = [255, 255, 255]
|
numpy_fg[~black_pixels,:] = [255, 255, 255]
|
||||||
image_fg = Image.fromarray(numpy_fg, image_rgb.mode)
|
image_fg = Image.fromarray(numpy_fg, image_rgb.mode)
|
||||||
|
|
||||||
# Background - white out all foreground pixels
|
# Background is only non-black
|
||||||
numpy_bg = numpy_rgb.copy()
|
numpy_bg = numpy_rgb.copy()
|
||||||
numpy_bg[fg_pixels,:] = [255, 255, 255]
|
numpy_bg[black_pixels,:] = [255, 255, 255]
|
||||||
image_bg = Image.fromarray(numpy_bg, image_rgb.mode)
|
image_bg = Image.fromarray(numpy_bg, image_rgb.mode)
|
||||||
|
|
||||||
# Handle case where empty background or foreground
|
# TODO: Handle case where empty background or foreground
|
||||||
if numpy.any(fg_pixels):
|
|
||||||
return SegmentedPage(fg=image_fg, bg=image_bg)
|
return SegmentedPage(fg=image_fg, bg=image_bg)
|
||||||
else:
|
|
||||||
return SegmentedPage(fg=None, bg=image_bg)
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user