Module: Mindee::PDF::PDFCompressor

Defined in:
lib/mindee/pdf/pdf_compressor.rb,
sig/mindee/pdf/pdf_compressor.rbs

Overview

Image compressor module to handle PDF compression.

Class Method Summary collapse

Class Method Details

.compress_pdf(pdf_data, quality: 85, force_source_text_compression: false, disable_source_text: true) ⇒ StringIO, File

Compresses each page of a provided PDF stream. Skips if force_source_text isn't set and source text is detected.

Parameters:

  • pdf_data (StringIO)

    StringIO handle of the file.

  • quality (Integer) (defaults to: 85)

    Compression quality (70-100 for most JPG images in the test dataset).

  • force_source_text_compression (bool) (defaults to: false)

    If true, attempts to re-write detected text.

  • disable_source_text (bool) (defaults to: true)

    If true, doesn't re-apply source text to the original PDF.

  • (StringIO, File)
  • quality: (Integer) (defaults to: 85)
  • force_source_text_compression: (Boolean) (defaults to: false)
  • disable_source_text: (Boolean) (defaults to: true)

Returns:

  • (StringIO, File)


18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# File 'lib/mindee/pdf/pdf_compressor.rb', line 18

def self.compress_pdf(pdf_data, quality: 85, force_source_text_compression: false, disable_source_text: true)
  if PDFTools.source_text?(pdf_data)
    if force_source_text_compression
      if disable_source_text
        logger.warn('Re-writing PDF source-text is an EXPERIMENTAL feature.')
      else
        logger.warn('Source-file contains text, but disable_source_text flag is ignored. ' \
                    'Resulting file will not contain any embedded text.')
      end
    else
      logger.warn('Source-text detected in input PDF. Aborting operation.')
      return pdf_data
    end
  end

  pdf_data.rewind
  pdf = Origami::PDF.read(pdf_data)
  pages = process_pdf_pages(pdf, quality)

  output_pdf = create_output_pdf(pages, disable_source_text, pdf_data)

  output_stream = StringIO.new
  output_pdf.save(output_stream)
  output_stream
end

.create_output_pdf(pages, disable_source_text, pdf_data) ⇒ Origami::PDF

Creates the output PDF with processed pages.

Parameters:

  • pages (Array<Origami::Page>)

    Processed pages.

  • disable_source_text (bool)

    Whether to disable source text.

  • pdf_data (StringIO)

    Original PDF data.

  • (Array[Origami::Page])
  • (Boolean)
  • (StringIO, File)

Returns:



60
61
62
63
64
65
66
67
68
69
# File 'lib/mindee/pdf/pdf_compressor.rb', line 60

def self.create_output_pdf(pages, disable_source_text, pdf_data)
  output_pdf = Origami::PDF.new
  pages.rotate!(1) if pages.count >= 2

  inject_text(pdf_data, pages) unless disable_source_text

  pages.each { |page| output_pdf.append_page(page) }

  output_pdf
end

.inject_text(pdf_data, pages) ⇒ nil

Extracts text from a source text PDF, and injects it into a newly-created one.

Parameters:

  • pdf_data (StringIO)

    Stream representation of the PDF.

  • pages (Array<Origami::Page>)

    Array of pages containing the rasterized version of the initial pages.

  • (StringIO, File)
  • (Array[Origami::Page])

Returns:

  • (nil)


74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'lib/mindee/pdf/pdf_compressor.rb', line 74

def self.inject_text(pdf_data, pages)
  reader = PDFReader::Reader.new(pdf_data)

  reader.pages.each_with_index do |original_page, index|
    break if index >= pages.length

    receiver = PDFReader::Reader::PageTextReceiver.new
    original_page.walk(receiver)

    receiver.runs.each do |text_run|
      x = text_run.origin.x
      y = text_run.origin.y
      text = text_run.text
      font_size = text_run.font_size

      content_stream = Origami::Stream.new
      content_stream.dictionary[:Filter] = :FlateDecode
      content_stream.data = "BT\n/F1 #{font_size} Tf\n#{x} #{y} Td\n(#{text}) Tj\nET\n"

      pages[index].Contents.data += content_stream.data
    end
  end
end

.loggerLogger

Returns:

  • (Logger)


7
# File 'sig/mindee/pdf/pdf_compressor.rbs', line 7

def self.logger: () -> Logger

.process_pdf_page(page_stream, page_index, image_quality, media_box) ⇒ Origami::Page

Takes in a page stream, rasterizes it into a JPEG image, and applies the result onto a new Origami PDF page.

Parameters:

  • page_stream (StringIO)

    Stream representation of a single page from the initial PDF.

  • page_index (Integer)

    Index of the current page. Technically not needed, but left for debugging purposes.

  • image_quality (Integer)

    Quality to apply to the rasterized page.

  • media_box (Array<Integer>, nil)

    Extracted media box from the page. Can be nil.

  • (StringIO)
  • (Integer)
  • (Integer)
  • (Array[Integer], nil)

Returns:



104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/mindee/pdf/pdf_compressor.rb', line 104

def self.process_pdf_page(page_stream, page_index, image_quality, media_box)
  new_page = Origami::Page.new
  compressed_image = Mindee::Image::ImageUtils.pdf_to_magick_image(page_stream, image_quality)
  width, height = Mindee::Image::ImageUtils.calculate_dimensions_from_media_box(compressed_image, media_box)

  compressed_xobject = PDF::PDFTools.create_xobject(compressed_image)
  PDF::PDFTools.set_xobject_properties(compressed_xobject, compressed_image)

  xobject_name = "X#{page_index + 1}"
  PDF::PDFTools.add_content_to_page(new_page, xobject_name, width, height)
  new_page.add_xobject(compressed_xobject, xobject_name)

  PDF::PDFTools.set_page_dimensions(new_page, width, height)
  new_page
end

.process_pdf_pages(pdf, quality) ⇒ Array<Origami::Page>

Processes all pages in the PDF.

Parameters:

  • pdf (Origami::PDF)

    The Origami PDF object to process.

  • quality (Integer)

    Compression quality.

  • (Origami::PDF)
  • (Integer)

Returns:



48
49
50
51
52
53
# File 'lib/mindee/pdf/pdf_compressor.rb', line 48

def self.process_pdf_pages(pdf, quality)
  pdf.pages.map.with_index do |page, index|
    retrieved_page = Mindee::PDF::PDFProcessor.get_page(pdf, index)
    process_pdf_page(retrieved_page, index, quality, page[:MediaBox])
  end
end