bidi2pdf 0.1.9 → 0.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/bidi2pdf/cli.rb CHANGED
@@ -74,6 +74,8 @@ module Bidi2pdf
74
74
  option :page_ranges, type: :array, desc: "Page ranges to print (e.g., 1-2 4 6)"
75
75
  option :scale, type: :numeric, default: 1.0, desc: "Scale between 0.1 and 2.0"
76
76
  option :shrink_to_fit, type: :boolean, default: true, desc: "Shrink content to fit page"
77
+ option :generate_tagged_pdf, type: :boolean, default: false, desc: "Generate tagged PDF"
78
+ option :generate_document_outline, type: :boolean, default: false, desc: "Generate document outline"
77
79
 
78
80
  class << self
79
81
  def exit_on_failure?
@@ -150,7 +152,7 @@ module Bidi2pdf
150
152
  raise Thor::Error, "Invalid print option: #{e.message}"
151
153
  end
152
154
 
153
- # rubocop:disable Metrics/CyclomaticComplexity
155
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
154
156
  def print_options
155
157
  opts = {}
156
158
 
@@ -186,10 +188,15 @@ module Bidi2pdf
186
188
  assign_if_provided(page, :height, :page_height)
187
189
  opts[:page] = page unless page.empty?
188
190
 
191
+ assign_if_provided(opts, :generate_tagged_pdf)
192
+ assign_if_provided(opts, :generate_document_outline)
193
+
194
+ opts[:cmd_type] = :cdp if opts[:generate_tagged_pdf] || opts[:generate_document_outline]
195
+
189
196
  opts.empty? ? nil : opts
190
197
  end
191
198
 
192
- # rubocop:enable Metrics/CyclomaticComplexity
199
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
193
200
 
194
201
  def option_provided?(key)
195
202
  ARGV.include?("--#{key.to_s.tr("_", "-")}") || ARGV.include?("--#{key}")
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bidi2pdf
4
+ module TestHelpers
5
+ class Configuration
6
+ # @!attribute [rw] spec_dir
7
+ # @return [Pathname] the directory where specs are located
8
+ attr_accessor :spec_dir
9
+
10
+ # @!attribute [rw] tmp_dir
11
+ # @return [String] the directory for temporary files
12
+ attr_accessor :tmp_dir
13
+
14
+ # @!attribute [rw] prefix
15
+ # @return [String] the prefix for temporary files
16
+ attr_accessor :prefix
17
+
18
+ # @!attribute [rw] docker_dir
19
+ # @return [String] the directory for Docker files
20
+ attr_accessor :docker_dir
21
+
22
+ # @!attribute [rw] fixture_dir
23
+ # @return [String] the directory for fixture files
24
+ attr_accessor :fixture_dir
25
+
26
+ def initialize
27
+ project_root = if defined?(Rails) && Rails.respond_to?(:root)
28
+ Pathname.new(Rails.root)
29
+ elsif defined?(Bundler) && Bundler.respond_to?(:root)
30
+ Pathname.new(Bundler.root)
31
+ else
32
+ Pathname.new(Dir.pwd)
33
+ end
34
+
35
+ @spec_dir = project_root.join("spec").expand_path
36
+ @docker_dir = project_root.join("docker")
37
+ @fixture_dir = project_root.join("spec", "fixtures")
38
+ @tmp_dir = project_root.join("tmp")
39
+ @prefix = "tmp_"
40
+ end
41
+ end
42
+
43
+ class << self
44
+ # Retrieves the current configuration object for TestHelpers.
45
+ # @return [Configuration] the configuration object
46
+ def configuration
47
+ @configuration ||= Configuration.new
48
+ end
49
+
50
+ # Sets the configuration object for TestHelpers.
51
+ # @param [Configuration] config the configuration object to set
52
+ attr_writer :configuration
53
+
54
+ # Allows configuration of TestHelpers by yielding the configuration object.
55
+ # @yieldparam [Configuration] configuration the configuration object to modify
56
+ def configure
57
+ yield(configuration)
58
+ end
59
+ end
60
+ end
61
+
62
+ # Configures RSpec to include and extend SpecPathsHelper for examples with the `:pdf` metadata.
63
+ RSpec.configure do |config|
64
+ # Adds a custom RSpec setting for TestHelpers configuration.
65
+ config.add_setting :bidi2pdf_test_helpers_config, default: TestHelpers.configuration
66
+ end
67
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bidi2pdf
4
+ module TestHelpers
5
+ module Images
6
+ require "vips"
7
+ require "zlib"
8
+
9
+ class Extractor
10
+ include PDFReaderUtils
11
+ include TIFFHelper
12
+
13
+ attr_reader :pages, :logger
14
+
15
+ def initialize(pdf_data, logger: Bidi2pdf.logger)
16
+ reader = pdf_reader_for pdf_data
17
+ @pages = reader.pages
18
+ @logger = logger
19
+ end
20
+
21
+ def all_images
22
+ extracted_images.map { |images| images[:images] }.flatten
23
+ end
24
+
25
+ def image_on_page(page_number, image_number)
26
+ images = images_on_page(page_number)
27
+ return nil if images.empty? || image_number > images.size
28
+
29
+ images[image_number - 1]
30
+ end
31
+
32
+ def images_on_page(page_number)
33
+ extracted_images.find { |images| images[:page] == page_number }&.dig(:images) || []
34
+ end
35
+
36
+ private
37
+
38
+ def extracted_images
39
+ @extracted_images ||= @pages.each_with_index.with_object([]) do |(page, index), result|
40
+ result << { page: index + 1, images: extract_images(page) }
41
+ end
42
+ end
43
+
44
+ def extract_images(page)
45
+ xobjects = page.xobjects
46
+ return if xobjects.empty?
47
+
48
+ xobjects.each_value.map do |stream|
49
+ case stream.hash[:Subtype]
50
+ when :Image
51
+ process_image_stream(stream)
52
+ when :Form
53
+ extract_images(PDF::Reader::FormXObject.new(page, stream))
54
+ end
55
+ end.flatten
56
+ end
57
+
58
+ def process_image_stream(stream)
59
+ filter = Array(stream.hash[:Filter]).first
60
+ raw = extract_raw_image_data(stream, filter)
61
+
62
+ return nil if raw.nil? || raw.empty?
63
+
64
+ create_vips_image(raw, filter)
65
+ end
66
+
67
+ def extract_raw_image_data(stream, filter)
68
+ case filter
69
+ when :DCTDecode, :JPXDecode then stream.data
70
+ when :CCITTFaxDecode then tiff_header_for_CCITT(stream.hash, stream.data)
71
+ when :LZWDecode, :RunLengthDecode, :FlateDecode then handle_compressed_image(stream)
72
+ else
73
+ Bidi2pdf.logger.warn("Unsupported image filter '#{filter}'. Attempting to process raw data.")
74
+ stream.data
75
+ end
76
+ rescue StandardError => e
77
+ Bidi2pdf.logger.error("Error extracting raw image data with filter '#{filter}': #{e.message}")
78
+ nil # Return nil to indicate failure
79
+ end
80
+
81
+ def handle_compressed_image(stream)
82
+ hash = stream.hash
83
+ data = stream.unfiltered_data
84
+
85
+ header = tiff_header(hash, data)
86
+
87
+ header + data
88
+ end
89
+
90
+ def create_vips_image(raw, filter)
91
+ Vips::Image.new_from_buffer(raw, "", disc: true)
92
+ rescue Vips::Error => e
93
+ Bidi2pdf.logger.error("Error creating Vips image from buffer (filter: #{filter}): #{e.message}")
94
+ nil # Return nil if Vips fails
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bidi2pdf
4
+ module TestHelpers
5
+ module Images
6
+ require "dhash-vips"
7
+
8
+ class ImageSimilarityChecker
9
+ def initialize(expected_image, image_to_check)
10
+ @expected_image = expected_image.is_a?(Vips::Image) ? expected_image : Vips::Image.new_from_file(expected_image)
11
+ @image_to_check = image_to_check.is_a?(Vips::Image) ? image_to_check : Vips::Image.new_from_file(image_to_check)
12
+ end
13
+
14
+ def similar?(tolerance: 20)
15
+ distance < tolerance
16
+ end
17
+
18
+ def very_similar?
19
+ similar? tolerance: 20
20
+ end
21
+
22
+ def slightly_similar?
23
+ similar? tolerance: 25
24
+ end
25
+
26
+ def different?
27
+ !slightly_similar?
28
+ end
29
+
30
+ def expected_fingerprint
31
+ @expected_fingerprint ||= fingerprint @expected_image
32
+ end
33
+
34
+ def actual_fingerprint
35
+ @actual_fingerprint ||= fingerprint @image_to_check
36
+ end
37
+
38
+ def distance
39
+ @distance ||= DHashVips::IDHash.distance(expected_fingerprint, actual_fingerprint)
40
+ end
41
+
42
+ def fingerprint(image)
43
+ image = image.resize(32.0 / [image.width, image.height].min) if image.width < 32 || image.height < 32
44
+
45
+ DHashVips::IDHash.fingerprint image
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,204 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bidi2pdf
4
+ module TestHelpers
5
+ module Images
6
+ # rubocop: disable Metrics/ModuleLength, Metrics/AbcSize
7
+ module TIFFHelper
8
+ # TIFF Tag IDs
9
+ IMAGE_WIDTH = 256
10
+ IMAGE_LENGTH = 257
11
+ BITS_PER_SAMPLE = 258
12
+ COMPRESSION = 259
13
+ PHOTOMETRIC_INTERPRETATION = 262
14
+ STRIP_OFFSETS = 273
15
+ SAMPLES_PER_PIXEL = 277
16
+ ROWS_PER_STRIP = 278
17
+ STRIP_BYTE_COUNTS = 279
18
+ PLANAR_CONFIGURATION = 284
19
+ INK_SET = 332
20
+
21
+ # TIFF Data Types
22
+ TYPE_SHORT = 3
23
+ TYPE_LONG = 4
24
+
25
+ # TIFF Compression Types
26
+ COMPRESSION_NONE = 1
27
+ COMPRESSION_CCITT_G3 = 3
28
+ COMPRESSION_CCITT_G4 = 4
29
+
30
+ # TIFF Photometric Interpretations
31
+ PHOTO_WHITE_IS_ZERO = 0
32
+ PHOTO_BLACK_IS_ZERO = 1
33
+ PHOTO_RGB = 2
34
+ PHOTO_SEPARATION = 5
35
+
36
+ # Planar Configuration
37
+ PLANAR_CHUNKY = 1
38
+
39
+ def tiff_header(hash, data)
40
+ cs_entry = hash[:ColorSpace]
41
+
42
+ cs = if cs_entry.is_a?(Array) && cs_entry.first == :ICCBased
43
+ icc_stream = cs_entry[1]
44
+ icc_stream.hash[:Alternate]
45
+ else
46
+ cs_entry
47
+ end
48
+
49
+ case cs
50
+ when :DeviceCMYK then tiff_header_for_CMYK(hash, data)
51
+ when :DeviceGray then tiff_header_for_gray(hash, data)
52
+ when :DeviceRGB then tiff_header_for_rgb(hash, data)
53
+ else
54
+ logger.warn("Unsupported color space '#{cs}' for compressed image with filter '#{hash[:Filter]}'. Skipping image.")
55
+ nil # Skip processing this image
56
+ end
57
+ end
58
+
59
+ # See:
60
+ # * https://gist.github.com/gstorer/f6a9f1dfe41e8e64dcf58d07afa9ab2a
61
+ # * https://github.com/yob/pdf-reader/blob/main/examples/extract_images.rb
62
+ def pack_tiff(entries)
63
+ fields = entries.size
64
+ fmt = [
65
+ "a2", # Byte order ("II")
66
+ "S<", # TIFF magic (42)
67
+ "L<", # Offset to first IFD (8)
68
+ "S<", # Number of directory entries
69
+ ("S< S< L< L<" * fields), # each tag: id, type, count, value
70
+ "L<" # Next IFD offset (0 = end)
71
+ ].join(" ")
72
+
73
+ # Build the flat array: ['II', 42, 8, fields, tag1, type1, count1, value1, …, 0]
74
+ values = ["II", 42, 8, fields] + entries.flatten + [0]
75
+ values.pack(fmt)
76
+ end
77
+
78
+ def tiff_header_for_gray(hash, data)
79
+ width = hash[:Width]
80
+ height = hash[:Height]
81
+ bpc = hash[:BitsPerComponent] || 8
82
+ img_size = data.bytesize
83
+
84
+ # 9 tags, no extra arrays needed
85
+ fields = 9
86
+ # size of header+IFD before the image data starts
87
+ header_ifd_size = 2 + 2 + 4 + 2 + (fields * 12) + 4
88
+ data_offset = header_ifd_size
89
+
90
+ entries = [
91
+ [IMAGE_WIDTH, TYPE_LONG, 1, width], # ImageWidth
92
+ [IMAGE_LENGTH, TYPE_LONG, 1, height], # ImageLength
93
+ [BITS_PER_SAMPLE, TYPE_SHORT, 1, bpc], # BitsPerSample
94
+ [COMPRESSION, TYPE_SHORT, 1, COMPRESSION_NONE], # Compression (1 = none)
95
+ [PHOTOMETRIC_INTERPRETATION, TYPE_SHORT, 1, PHOTO_BLACK_IS_ZERO], # PhotometricInterpretation (1 = BlackIsZero)
96
+ [STRIP_OFFSETS, TYPE_LONG, 1, data_offset], # StripOffsets
97
+ [SAMPLES_PER_PIXEL, TYPE_SHORT, 1, 1], # SamplesPerPixel
98
+ [STRIP_BYTE_COUNTS, TYPE_LONG, 1, img_size], # StripByteCounts
99
+ [PLANAR_CONFIGURATION, TYPE_SHORT, 1, PLANAR_CHUNKY] # PlanarConfiguration (1 = chunky)
100
+ ]
101
+
102
+ pack_tiff(entries)
103
+ end
104
+
105
+ def tiff_header_for_ccitt(hash, data)
106
+ dp = hash[:DecodeParms] || {}
107
+ width = dp[:Columns] || hash[:Width]
108
+ height = hash[:Height]
109
+ k = dp[:K] || 0
110
+ group = (k.positive? ? COMPRESSION_CCITT_G3 : COMPRESSION_CCITT_G4)
111
+ img_size = data.bytesize
112
+
113
+ # We’ll emit exactly 8 tags:
114
+ fields = 8
115
+ # Calculate where the image data will start:
116
+ header_size = 2 + 2 + 4 + 2 + (fields * 12) + 4
117
+
118
+ entries = [
119
+ [IMAGE_WIDTH, TYPE_LONG, 1, width], # ImageWidth
120
+ [IMAGE_LENGTH, TYPE_LONG, 1, height], # ImageLength
121
+ [BITS_PER_SAMPLE, TYPE_SHORT, 1, 1], # BitsPerSample
122
+ [COMPRESSION, TYPE_SHORT, 1, group], # Compression (3=G3, 4=G4)
123
+ [PHOTOMETRIC_INTERPRETATION, TYPE_SHORT, 1, PHOTO_WHITE_IS_ZERO], # PhotometricInterpretation (0 = WhiteIsZero)
124
+ [STRIP_OFFSETS, TYPE_LONG, 1, header_size], # StripOffsets
125
+ [ROWS_PER_STRIP, TYPE_LONG, 1, height], # RowsPerStrip
126
+ [STRIP_BYTE_COUNTS, TYPE_LONG, 1, img_size] # StripByteCounts
127
+ ]
128
+
129
+ pack_tiff(entries)
130
+ end
131
+
132
+ def tiff_header_for_cmyk(hash, data)
133
+ width = hash[:Width]
134
+ height = hash[:Height]
135
+ bpc = hash[:BitsPerComponent] || 8
136
+ img_size = data.bytesize
137
+
138
+ # CMYK needs 10 tags + a 4×SHORT BitsPerSample array
139
+ fields = 10
140
+ bits_array_size = 4 * 2 # 4 channels × 2 bytes each
141
+
142
+ # Size of header + IFD (before the bits array)
143
+ header_ifd_size = 2 + 2 + 4 + 2 + (fields * 12) + 4
144
+ # Where the pixel data will really start:
145
+ data_offset = header_ifd_size + bits_array_size
146
+
147
+ entries = [
148
+ [IMAGE_WIDTH, TYPE_LONG, 1, width], # ImageWidth
149
+ [IMAGE_LENGTH, TYPE_LONG, 1, height], # ImageLength
150
+ [BITS_PER_SAMPLE, TYPE_SHORT, 4, header_ifd_size], # BitsPerSample (pointer to array)
151
+ [COMPRESSION, TYPE_SHORT, 1, COMPRESSION_NONE], # Compression (1 = none)
152
+ [PHOTOMETRIC_INTERPRETATION, TYPE_SHORT, 1, PHOTO_SEPARATION], # PhotometricInterpretation (5 = Separation)
153
+ [STRIP_OFFSETS, TYPE_LONG, 1, data_offset], # StripOffsets
154
+ [SAMPLES_PER_PIXEL, TYPE_SHORT, 1, 4], # SamplesPerPixel
155
+ [STRIP_BYTE_COUNTS, TYPE_LONG, 1, img_size], # StripByteCounts
156
+ [PLANAR_CONFIGURATION, TYPE_SHORT, 1, PLANAR_CHUNKY], # PlanarConfiguration (1 = chunky)
157
+ [INK_SET, TYPE_SHORT, 1, 1] # InkSet (1 = CMYK)
158
+ ]
159
+
160
+ header = pack_tiff(entries)
161
+ # Append the 4-channel BitsPerSample array as little‐endian SHORTs:
162
+ header << [bpc, bpc, bpc, bpc].pack("S<S<S<S<")
163
+ header
164
+ end
165
+
166
+ def tiff_header_for_rgb(hash, data)
167
+ width = hash[:Width]
168
+ height = hash[:Height]
169
+ bpc = hash[:BitsPerComponent] || 8
170
+ img_size = data.bytesize
171
+
172
+ # 8 tags + a 3×SHORT BitsPerSample array
173
+ fields = 8
174
+ bits_array_size = 3 * 2 # 3 channels × 2 bytes each
175
+
176
+ # size of header + IFD before the bits array
177
+ header_ifd_size = 2 + 2 + 4 + 2 + (fields * 12) + 4
178
+ # where the pixel data really starts:
179
+ data_offset = header_ifd_size + bits_array_size
180
+
181
+ entries = [
182
+ [IMAGE_WIDTH, TYPE_LONG, 1, width], # ImageWidth
183
+ [IMAGE_LENGTH, TYPE_LONG, 1, height], # ImageLength
184
+ [BITS_PER_SAMPLE, TYPE_SHORT, 3, header_ifd_size], # BitsPerSample → pointer to our array
185
+ [COMPRESSION, TYPE_SHORT, 1, COMPRESSION_NONE], # Compression (1 = none)
186
+ [PHOTOMETRIC_INTERPRETATION, TYPE_SHORT, 1, PHOTO_RGB], # PhotometricInterpretation (2 = RGB)
187
+ [STRIP_OFFSETS, TYPE_LONG, 1, data_offset], # StripOffsets
188
+ [SAMPLES_PER_PIXEL, TYPE_SHORT, 1, 3], # SamplesPerPixel
189
+ [STRIP_BYTE_COUNTS, TYPE_LONG, 1, img_size] # StripByteCounts
190
+ ]
191
+
192
+ # pack the IFD
193
+ header = pack_tiff(entries)
194
+
195
+ # append the 3-channel BitsPerSample as little-endian SHORTs
196
+ header << [bpc, bpc, bpc].pack("S<S<S<")
197
+
198
+ header
199
+ end
200
+ end
201
+ end
202
+ # rubocop: enable Metrics/ModuleLength, Metrics/AbcSize
203
+ end
204
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ %w[vips dhash-vips].each do |dep|
4
+ require dep
5
+ rescue LoadError
6
+ warn "Missing #{dep}. Add it to your Gemfile if you're using Bidi2pdf image test helpers."
7
+ end
8
+
9
+ require_relative "images/tiff_helper"
10
+ require_relative "images/extractor"
11
+ require_relative "images/image_similarity_checker"
12
+ require_relative "matchers/contains_pdf_image"
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec::Matchers.define :contains_pdf_image do |expected, tolerance: 10|
4
+ chain :at_page do |page_number|
5
+ @page_number = page_number
6
+ end
7
+
8
+ chain :at_position do |i|
9
+ @image_number = i
10
+ end
11
+
12
+ match do |actual_pdf|
13
+ extractor = Bidi2pdf::TestHelpers::Images::Extractor.new(actual_pdf)
14
+ @images = if @page_number
15
+ @image_number ? [extractor.image_on_page(@page_number, @image_number)].compact : extractor.images_on_page(@page_number)
16
+ else
17
+ extractor.all_images
18
+ end
19
+
20
+ @checkers = @images.map { |image| Bidi2pdf::TestHelpers::Images::ImageSimilarityChecker.new(expected, image) }
21
+
22
+ @checkers.any? { |checker| checker.similar?(tolerance:) }
23
+ end
24
+
25
+ failure_message do |_actual_pdf|
26
+ "expected to find one image #{"on page #{@page_number}" if @page_number}#{" at position #{@image_number}" if @image_number} to be perceptually similar (distance ≤ #{tolerance}), " \
27
+ "but Hamming distances have been #{@checkers.map(&:distance).join(", ")}"
28
+ end
29
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bidi2pdf
4
+ module TestHelpers
5
+ # This module provides helper methods for handling PDF files in tests.
6
+ # It includes methods for debugging, storing, and managing PDF files.
7
+ module PdfFileHelper
8
+ # Executes a block with the given PDF data and handles debugging in case of test failures.
9
+ # If an expectation fails, the PDF data is saved to a file for debugging purposes.
10
+ # @param [String] pdf_data the PDF data to debug
11
+ # @yield [String] yields the PDF data to the given block
12
+ # @raise [RSpec::Expectations::ExpectationNotMetError] re-raises the exception after saving the PDF
13
+ def with_pdf_debug(pdf_data)
14
+ yield pdf_data
15
+ rescue RSpec::Expectations::ExpectationNotMetError => e
16
+ failure_output = store_pdf_file pdf_data, "test-failure"
17
+ puts "Test failed! PDF saved to: #{failure_output}"
18
+ raise e
19
+ end
20
+
21
+ # Stores the given PDF data to a file with a specified filename prefix.
22
+ # The file is saved in a temporary directory.
23
+ # @param [String] pdf_data the PDF data to store
24
+ # @param [String] filename_prefix the prefix for the generated filename (default: "test")
25
+ # @return [String] the full path to the saved PDF file
26
+ def store_pdf_file(pdf_data, filename_prefix = "test")
27
+ pdf_file = tmp_file("pdf-files", "#{filename_prefix}-#{Time.now.to_i}.pdf")
28
+ FileUtils.mkdir_p(File.dirname(pdf_file))
29
+ File.binwrite(pdf_file, pdf_data)
30
+
31
+ pdf_file
32
+ end
33
+ end
34
+
35
+ RSpec.configure do |config|
36
+ config.include PdfFileHelper, pdf: true
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This module provides helper methods for managing paths in test environments.
4
+ # It includes methods to retrieve directories and generate temporary file paths.
5
+ module Bidi2pdf
6
+ module TestHelpers
7
+ # This submodule contains path-related helper methods and configuration for tests.
8
+ module SpecPathsHelper
9
+ # Retrieves the directory path for Docker files.
10
+ # @return [String] the Docker directory path
11
+ def fixture_dir
12
+ TestHelpers.configuration.fixture_dir
13
+ end
14
+
15
+ # Retrieves the directory path for fixtures.
16
+ # @return [String] the fixture directory path
17
+ def fixture_file(*)
18
+ File.join(fixture_dir, *)
19
+ end
20
+
21
+ # Retrieves the directory path for specs.
22
+ # @return [String] the spec directory path
23
+ def spec_dir
24
+ TestHelpers.configuration.spec_dir
25
+ end
26
+
27
+ # Retrieves the directory path for temporary files.
28
+ # @return [String] the temporary directory path
29
+ def tmp_dir
30
+ TestHelpers.configuration.tmp_dir
31
+ end
32
+
33
+ # Generates a path for a temporary file by joining the temporary directory with the given parts.
34
+ # @param [Array<String>] parts the parts of the file path to join
35
+ # @return [String] the full path to the temporary file
36
+ def tmp_file(*parts)
37
+ File.join(tmp_dir, *parts)
38
+ end
39
+
40
+ # Generates a random temporary directory path.
41
+ # @param [Array<String>] dirs additional directory components to include in the path
42
+ # @param [String, nil] prefix an optional prefix for the directory name
43
+ # @return [String] the full path to the random temporary directory
44
+ def random_tmp_dir(*dirs, prefix: nil)
45
+ base_dirs = [tmp_dir] + dirs.compact
46
+ pfx = prefix || TestHelpers.configuration.prefix
47
+ File.join(*base_dirs, "#{pfx}#{SecureRandom.hex(8)}")
48
+ end
49
+ end
50
+
51
+ # Configures RSpec to include and extend SpecPathsHelper for examples with the `:pdf` metadata.
52
+ RSpec.configure do |config|
53
+ # Includes SpecPathsHelper methods in examples with `:pdf` metadata.
54
+ config.include SpecPathsHelper, pdf: true
55
+
56
+ # Extends SpecPathsHelper methods to example groups with `:pdf` metadata.
57
+ config.extend SpecPathsHelper, pdf: true
58
+ end
59
+ end
60
+ end
@@ -46,7 +46,7 @@ RSpec.configure do |config|
46
46
  config.before(:suite) do
47
47
  if chromedriver_tests_present?
48
48
  config.chromedriver_container = start_chromedriver_container(
49
- build_dir: File.join(config.docker_dir, ".."),
49
+ build_dir: File.join(Bidi2pdf::TestHelpers.configuration.docker_dir, ".."),
50
50
  mounts: config.respond_to?(:chromedriver_mounts) ? config.chromedriver_mounts : {},
51
51
  shared_network: config.shared_network
52
52
  )
@@ -31,7 +31,7 @@ module Bidi2pdf
31
31
 
32
32
  def _container_create_options
33
33
  opts = super
34
- network_name = network ? network.info["Name"] : nil
34
+ network_name = network&.info&.[]("Name")
35
35
  opts["HostConfig"]["NetworkMode"] = network_name
36
36
 
37
37
  if network && aliases.any?
@@ -6,8 +6,15 @@ rescue LoadError
6
6
  warn "Missing #{dep}. Add it to your Gemfile if you're using Bidi2pdf test helpers."
7
7
  end
8
8
 
9
+ require "bidi2pdf/test_helpers/configuration"
10
+ require "bidi2pdf/test_helpers/pdf_file_helper"
11
+ require "bidi2pdf/test_helpers/spec_paths_helper"
9
12
  require "bidi2pdf/test_helpers/pdf_text_sanitizer"
10
13
  require "bidi2pdf/test_helpers/pdf_reader_utils"
11
14
  require "bidi2pdf/test_helpers/matchers/match_pdf_text"
12
15
  require "bidi2pdf/test_helpers/matchers/contains_pdf_text"
13
16
  require "bidi2pdf/test_helpers/matchers/have_pdf_page_count"
17
+
18
+ # don't require "bidi2pdf/test_helpers/matchers/contains_pdf_image.rb" directly, use
19
+ # require "bidi2pdf/test_helpers/images" instead, because it requires
20
+ # ruby-vips and dhash-vips, and not every one wants to use them
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bidi2pdf
4
- VERSION = "0.1.9"
4
+ VERSION = "0.1.10"
5
5
  end