plaintext 0.3.7 → 0.4.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ffcae76367d81bd94ad8e2ad40b0a59a00b0aa0d00ef8790b8e72e96c37c8a5d
4
- data.tar.gz: 343182ccc4861b94ca0ac4e1733b60e44e60a840347b1eb877acaf9a4f54f440
3
+ metadata.gz: 4cff613a25c31b858cbe360c7311e468933d0714e9f80e217a25e3c74e9e9d72
4
+ data.tar.gz: 2ab92de5c21eac147f36cac18ebb01248f2dfa1aeb44a3c470bc503f528d3635
5
5
  SHA512:
6
- metadata.gz: 99dfbc41ec270e97ce31332a6eb69ddd346212c8c74df85cc77c218bb257ece73dded27d3db8ee35799d76ba73596f38f7942b0e8d2962c5de9a93a643cbe794
7
- data.tar.gz: beb1371a6b51afe487dfb06574471b0eb3ce5755ab7a1c87ae8a4b609f2db8f0753406e775efe418b94d81e3e324346f8bdee43fd454dfa47b34334b454ec2e8
6
+ metadata.gz: ef16b7430e3344f5213fa217b5e17a0abb79fba36f89e89a2f2a3cfb3a0401a69abf590f36a512ba3e6f6f101892ddfe2a32c5a3c5e26bcff0273ccea0dc797c
7
+ data.tar.gz: 99a650e3fe37ff7c1f036389806241e9e3f4107c47c13a717a93714d6c8fa998dc4b56510d27e27b7cc9c7ebbae9ea1c45ddef34060a123a1dea3ed9105d0a80
data/CHANGELOG CHANGED
@@ -4,6 +4,47 @@ All notable changes to this project will be documented in this file.
4
4
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
5
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [Unreleased]
8
+
9
+ ## [0.4.0] - 2026-09-21
10
+
11
+ ### Added
12
+ - `Resolver#preserve_whitespace`: when set, the extracted text is returned
13
+ with the whitespace the handler emitted instead of being collapsed into
14
+ single spaces. Keeps the line structure for plain text and the formats
15
+ extracted by an external command (PDF, DOC, XLS, PPT, RTF, images); the
16
+ zipped XML handlers join their text elements with a space and are
17
+ unaffected. (#13)
18
+
19
+ ### Changed
20
+ - Replaced `String#mb_chars`, deprecated and to be removed in Rails 8.2, with
21
+ `unicode_normalize` and `truncate_bytes`. File handlers are now expected to
22
+ return UTF-8: `mb_chars` used to silently retag whatever it was given, which
23
+ is what hid the encoding bugs fixed below. Handlers shipped with this gem all
24
+ do, a custom handler returning binary strings now raises.
25
+ - An extraction command exiting with a non-zero status now raises
26
+ `Plaintext::CommandFailed`. The exit status used to be ignored, so a
27
+ misconfigured or failing command silently yielded an empty string for every
28
+ file of its type.
29
+
30
+ ### Fixed
31
+ - Extracted text no longer has its non-ASCII characters replaced by question
32
+ marks. Command output was read as ASCII-8BIT no matter what the command
33
+ actually produced, and only the pdftotext handler was exempt, so umlauts and
34
+ accented characters from doc, xls, ppt, rtf and image files were destroyed.
35
+ - Cutting the output at `max_plaintext_bytes` no longer corrupts the whole
36
+ text. A byte limit routinely cuts through a multi byte character; only that
37
+ character is replaced now instead of every non-ASCII character in the
38
+ document, and `Resolver#text` no longer raises `ArgumentError: invalid byte
39
+ sequence in UTF-8` in that case.
40
+ - RTF files no longer lose their accented characters. unrtf cannot be told to
41
+ write UTF-8 — its `--text` output is always Latin-1 — so its output is
42
+ converted rather than expected to be UTF-8.
43
+ - Extraction no longer hangs forever when unrtf writes its header but no end
44
+ marker. Scanning for the marker did not stop at the end of the stream.
45
+ - Corrected the unrtf and tesseract example configurations, which did not match
46
+ the defaults used by the gem.
47
+
7
48
  ## [0.3.7] - 2025-11-25
8
49
 
9
50
  ### Added
data/README.md CHANGED
@@ -44,6 +44,11 @@ Or install it yourself as:
44
44
  In a Rails application save `plaintext.yml.example` in `config/plaintext.yml` and overwrite the settings to
45
45
  your needs.
46
46
 
47
+ Extraction commands are expected to write UTF-8 to STDOUT. Byte sequences that
48
+ are not valid UTF-8 are replaced by a question mark, so make sure to pass
49
+ whatever switch your command needs to produce UTF-8 — the defaults shipped with
50
+ this gem already do.
51
+
47
52
  Then load that configuration file in an initializer. Add the following lines to `config/initializers/plaintext.rb`:
48
53
 
49
54
  ```ruby
@@ -109,9 +114,23 @@ catdoc:
109
114
  fulltext = Plaintext::Resolver.new(file, content_type).text
110
115
  ```
111
116
 
117
+ `text` returns `nil` if no handler matches the content type and raises
118
+ `Plaintext::CommandFailed` if an extraction command exits with a non-zero
119
+ status, for example because it was given a switch it does not support.
120
+
112
121
  To limit the number of bytes returned (default is 4MB), set the
113
122
  `max_plaintext_bytes` property on the resolver instance before calling `text`.
114
123
 
124
+ All whitespace is collapsed into single spaces by default. To keep the line
125
+ structure emitted by the extraction command instead, set `preserve_whitespace`
126
+ on the resolver instance before calling `text`. That keeps the document
127
+ structure for plain text and the formats extracted by an external command
128
+ (PDF, DOC, XLS, PPT, RTF and images), while the handlers for the zipped XML
129
+ formats (OpenDocument and Office Open XML) join their text elements with a
130
+ space. The text is returned as the handler emits it, so it may still contain
131
+ form feeds between PDF pages, `\r\n` line endings and leading or trailing
132
+ whitespace.
133
+
115
134
  ## License
116
135
 
117
136
  The `plaintext` gem is free software; you can redistribute it and/or modify it under the terms of the GNU General
@@ -4,22 +4,17 @@ module Plaintext
4
4
  module CodesetUtil
5
5
  def self.to_utf8(str, encoding)
6
6
  return str if str.nil?
7
- str.force_encoding('ASCII-8BIT')
8
- if str.empty?
9
- str.force_encoding('UTF-8')
10
- return str
11
- end
12
- enc = (encoding.nil? || encoding.size == 0) ? 'UTF-8' : encoding
7
+
8
+ enc = encoding.blank? ? 'UTF-8' : encoding
9
+ str.force_encoding(enc)
13
10
  if enc.upcase != 'UTF-8'
14
- str.force_encoding(enc)
15
11
  str = str.encode('UTF-8', invalid: :replace,
16
12
  undef: :replace, replace: '?')
17
- else
18
- str.force_encoding('UTF-8')
19
- if !str.valid_encoding?
20
- str = str.encode('US-ASCII', invalid: :replace,
21
- undef: :replace, replace: '?').encode('UTF-8')
22
- end
13
+ elsif !str.valid_encoding?
14
+ # only replace the invalid byte sequences, leave the rest of the
15
+ # string alone. Reading up to a byte limit routinely cuts through a
16
+ # multi byte character at the very end of the string.
17
+ str = str.scrub('?')
23
18
  end
24
19
  str
25
20
  end
@@ -10,11 +10,5 @@ module Plaintext
10
10
  @content_type = 'application/pdf'
11
11
  @command = Plaintext::Configuration['pdftotext'] || DEFAULT
12
12
  end
13
-
14
- protected
15
-
16
- def utf8_stream?
17
- true
18
- end
19
13
  end
20
14
  end
@@ -15,10 +15,17 @@ module Plaintext
15
15
  UNRTF_HEADER = "### Translation from RTF performed by UnRTF"
16
16
  END_MARKER = "-----------------\n"
17
17
 
18
+ # unrtf has no switch to make it write UTF-8: its --text output is always
19
+ # Latin-1, it ignores the locale, and it replaces anything it cannot map
20
+ # into Latin-1 with a question mark itself.
21
+ def output_encoding
22
+ 'ISO-8859-1'
23
+ end
24
+
18
25
  def read(io, max_size = nil)
19
26
  if line = io.read(UNRTF_HEADER.length)
20
27
  string = if line.starts_with? UNRTF_HEADER
21
- io.gets while $_ != END_MARKER
28
+ io.each_line { |l| break if l == END_MARKER }
22
29
  io.read max_size
23
30
  else
24
31
  if max_size.nil?
@@ -29,7 +36,7 @@ module Plaintext
29
36
  line[0,max_size]
30
37
  end
31
38
  end
32
- Plaintext::CodesetUtil.to_utf8 string, "ASCII-8BIT"
39
+ Plaintext::CodesetUtil.to_utf8 string, output_encoding
33
40
  end
34
41
  end
35
42
  end
@@ -3,6 +3,10 @@
3
3
  require 'pathname'
4
4
 
5
5
  module Plaintext
6
+ # Raised when an extraction command exits with a non-zero status. Whatever
7
+ # the command wrote to STDOUT before failing is discarded.
8
+ class CommandFailed < StandardError; end
9
+
6
10
  class ExternalCommandHandler < FileHandler
7
11
  # TODO: Extract this to a proper module
8
12
  # Executes the given command through IO.popen and yields an IO object
@@ -13,15 +17,16 @@ module Plaintext
13
17
  require 'fileutils'
14
18
 
15
19
  FILE_PLACEHOLDER = '__FILE__'.freeze
16
- DEFAULT_STREAM_ENCODING = 'ASCII-8BIT'.freeze
17
20
 
18
21
  def shellout(cmd, options = {}, &block)
19
22
  mode = "r+"
20
- IO.popen(cmd, mode) do |io|
21
- set_stream_encoding(io)
23
+ result = IO.popen(cmd, mode) do |io|
24
+ io.binmode
22
25
  io.close_write unless options[:write_stdin]
23
26
  block.call(io) if block_given?
24
27
  end
28
+ raise CommandFailed, "#{cmd.join(' ')} failed: #{$?}" unless $?.success?
29
+ result
25
30
  end
26
31
 
27
32
  def text(file, options = {})
@@ -43,32 +48,17 @@ module Plaintext
43
48
  new.available?
44
49
  end
45
50
 
46
- protected
47
-
48
- def utf8_stream?
49
- false
50
- end
51
-
52
51
  private
53
52
 
54
- def set_stream_encoding(io)
55
- return unless io.respond_to?(:set_encoding)
56
-
57
- if utf8_stream?
58
- io.set_encoding('UTF-8'.freeze)
59
- else
60
- io.set_encoding(DEFAULT_STREAM_ENCODING)
61
- end
53
+ # Encoding the command writes its output in. Commands that can be told to
54
+ # produce UTF-8 are configured to do so (see plaintext.yml.example), the
55
+ # output of those that cannot is converted by #read.
56
+ def output_encoding
57
+ 'UTF-8'
62
58
  end
63
59
 
64
60
  def read(io, max_size = nil)
65
- piece = io.read(max_size)
66
-
67
- if utf8_stream?
68
- piece
69
- else
70
- Plaintext::CodesetUtil.to_utf8 piece, DEFAULT_STREAM_ENCODING
71
- end
61
+ Plaintext::CodesetUtil.to_utf8 io.read(max_size), output_encoding
72
62
  end
73
63
  end
74
64
  end
@@ -6,6 +6,10 @@ module Plaintext
6
6
  # maximum length of returned plain text in bytes. Default: 4MB
7
7
  attr_accessor :max_plaintext_bytes
8
8
 
9
+ # keep the whitespace emitted by the handler instead of collapsing it
10
+ # into single spaces. Default: false
11
+ attr_accessor :preserve_whitespace
12
+
9
13
  class << self
10
14
  attr_accessor :cached_file_handlers
11
15
 
@@ -29,6 +33,7 @@ module Plaintext
29
33
  @file = file
30
34
  @content_type = content_type
31
35
  @max_plaintext_bytes = 4_194_304 # 4 megabytes
36
+ @preserve_whitespace = false
32
37
  end
33
38
 
34
39
 
@@ -38,10 +43,12 @@ module Plaintext
38
43
  if handler = find_handler and
39
44
  text = handler.text(@file, max_size: max_plaintext_bytes)
40
45
 
41
- text = +text
42
- text.gsub!(/\s+/m, ' ')
43
- text.strip!
44
- text.mb_chars.compose.limit(max_plaintext_bytes).to_s
46
+ unless preserve_whitespace
47
+ text = +text
48
+ text.gsub!(/\s+/m, ' ')
49
+ text.strip!
50
+ end
51
+ text.unicode_normalize(:nfc).truncate_bytes(max_plaintext_bytes, omission: nil)
45
52
  end
46
53
  end
47
54
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Plaintext
4
- VERSION = "0.3.7"
4
+ VERSION = "0.4.0"
5
5
  end
@@ -3,8 +3,12 @@
3
3
  #
4
4
  # Text extraction helper programs.
5
5
  #
6
- # commands should write the resulting plain text to STDOUT. Use __FILE__ as
7
- # placeholder for the file path. The values below are the defaults.
6
+ # commands should write the resulting plain text to STDOUT, encoded in UTF-8.
7
+ # Use __FILE__ as placeholder for the file path. The values below are the
8
+ # defaults.
9
+ #
10
+ # Byte sequences that are not valid UTF-8 are replaced by a question mark, so
11
+ # make sure to pass whatever switch your command needs to produce UTF-8.
8
12
 
9
13
  # apt-get install poppler-utils
10
14
  # pdftotext:
@@ -15,8 +19,12 @@
15
19
  # - '-'
16
20
 
17
21
  # apt-get install unrtf
22
+ # unrtf has no switch to write UTF-8, its --text output is always Latin-1 and
23
+ # is converted by the rtf handler. Replacing it with a command that does write
24
+ # UTF-8 will garble accented characters.
18
25
  # unrtf:
19
26
  # - /usr/bin/unrtf
27
+ # - --nopict
20
28
  # - --text
21
29
  # - __FILE__
22
30
 
@@ -37,5 +45,6 @@
37
45
  # apt-get install tesseract-ocr
38
46
  # tesseract:
39
47
  # - /usr/bin/tesseract
40
- # - -dutf-8
41
- # - __FILE__
48
+ # - __FILE__
49
+ # - stdout
50
+ # - quiet
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: plaintext
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.7
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jens Krämer
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: exe
12
12
  cert_chain: []
13
- date: 2025-11-25 00:00:00.000000000 Z
13
+ date: 2026-09-21 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: activesupport