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 +4 -4
- data/CHANGELOG +41 -0
- data/README.md +19 -0
- data/lib/plaintext/codeset_util.rb +8 -13
- data/lib/plaintext/file_handler/external_command_handler/pdf_handler.rb +0 -6
- data/lib/plaintext/file_handler/external_command_handler/rtf_handler.rb +9 -2
- data/lib/plaintext/file_handler/external_command_handler.rb +14 -24
- data/lib/plaintext/resolver.rb +11 -4
- data/lib/plaintext/version.rb +1 -1
- data/plaintext.yml.example +13 -4
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4cff613a25c31b858cbe360c7311e468933d0714e9f80e217a25e3c74e9e9d72
|
|
4
|
+
data.tar.gz: 2ab92de5c21eac147f36cac18ebb01248f2dfa1aeb44a3c470bc503f528d3635
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
@@ -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.
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
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
|
data/lib/plaintext/resolver.rb
CHANGED
|
@@ -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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
data/lib/plaintext/version.rb
CHANGED
data/plaintext.yml.example
CHANGED
|
@@ -3,8 +3,12 @@
|
|
|
3
3
|
#
|
|
4
4
|
# Text extraction helper programs.
|
|
5
5
|
#
|
|
6
|
-
# commands should write the resulting plain text to STDOUT
|
|
7
|
-
# placeholder for the file path. The values below are the
|
|
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
|
-
# -
|
|
41
|
-
# -
|
|
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.
|
|
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:
|
|
13
|
+
date: 2026-09-21 00:00:00.000000000 Z
|
|
14
14
|
dependencies:
|
|
15
15
|
- !ruby/object:Gem::Dependency
|
|
16
16
|
name: activesupport
|