pdfshaver 0.0.1.alpha1 → 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +8 -0
- data/Gemfile.lock +26 -0
- data/Readme.md +56 -3
- data/bench/data_loading_speed.rb +13 -0
- data/bench/memory_stress.rb +20 -0
- data/bench/setup.rb +53 -0
- data/ext/pdfium_ruby/document.cpp +6 -9
- data/ext/pdfium_ruby/extconf.rb +9 -4
- data/ext/pdfium_ruby/page.cpp +97 -62
- data/ext/pdfium_ruby/page.h +5 -1
- data/lib/pdfshaver/page.rb +41 -3
- data/pdfshaver.gemspec +14 -16
- data/test/gc_spec.rb +23 -0
- data/test/gm_compatability_spec.rb +2 -2
- data/test/page_spec.rb +39 -15
- metadata +38 -25
- data/test/fixtures/completely_encrypted.pdf +0 -0
- data/test/fixtures/encrypted.pdf +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 179892daf5c810a3516fef72ded8233423ebb6e7
|
4
|
+
data.tar.gz: ee7d17c718fff0ce34cec44de9f3abed6a86e2c2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 97b911682b430e6f8d4314e39563f1f9d23332143d9b0170465b35f26a5caf0466b71543cba257329f9016c7fc304b7d769956f87008beed5bd0f39024fe377e
|
7
|
+
data.tar.gz: 78f35366a38991906e6097f79a1b13751bcd1f6dc75caada0825588407246422c01ed59550eb45709b5e2480cce8febb683a8b8954af2bc0332b404830270251
|
data/.gitignore
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
pdfium (0.0.1)
|
5
|
+
|
6
|
+
GEM
|
7
|
+
remote: https://rubygems.org/
|
8
|
+
specs:
|
9
|
+
addressable (2.3.7)
|
10
|
+
fastimage (1.6.6)
|
11
|
+
addressable (~> 2.3, >= 2.3.5)
|
12
|
+
minitest (5.5.1)
|
13
|
+
rake (10.4.2)
|
14
|
+
rake-compiler (0.9.5)
|
15
|
+
rake
|
16
|
+
|
17
|
+
PLATFORMS
|
18
|
+
ruby
|
19
|
+
|
20
|
+
DEPENDENCIES
|
21
|
+
bundler (~> 1.5)
|
22
|
+
fastimage
|
23
|
+
minitest
|
24
|
+
pdfium!
|
25
|
+
rake
|
26
|
+
rake-compiler
|
data/Readme.md
CHANGED
@@ -1,14 +1,67 @@
|
|
1
1
|
# PDFShaver
|
2
2
|
|
3
|
-
|
3
|
+
# N.B. THIS IS A WORK IN PROGRESS
|
4
4
|
|
5
|
-
|
5
|
+
Shave pages off of PDFs as images
|
6
6
|
|
7
7
|
### Examples
|
8
8
|
|
9
9
|
require 'pdfshaver'
|
10
|
+
# open a document!
|
10
11
|
document = PDFShaver::Document.new('./path/to/document.pdf')
|
12
|
+
# Iterate through its pages
|
11
13
|
landscape_pages = document.pages.select{ |page| page.aspect > 1 }
|
12
14
|
landscape_pages.each{ |page| page.render("./page_#{page.number}.gif") }
|
13
15
|
|
14
|
-
copyright 2015 Ted Han, Nathan Stitt & DocumentCloud
|
16
|
+
copyright 2015 Ted Han, Nathan Stitt & DocumentCloud
|
17
|
+
|
18
|
+
## Installation
|
19
|
+
|
20
|
+
PDFShaver is distributed as a Ruby gem. Once you have it's dependencies installed, all you have to do is type `gem install pdfshaver` (although in some cases you'll need to stick a `sudo` before the command).
|
21
|
+
|
22
|
+
PDFShaver depends on [Google Chrome's `PDFium` library][pdfium], and for now, installing `PDFium` takes a little bit of doing.
|
23
|
+
|
24
|
+
[pdfium]: https://code.google.com/p/pdfium/
|
25
|
+
|
26
|
+
In order install PDFium, you'll need Python, a C++ compiler, FreeImage and `git`. All of these tools should be available for your operating system.
|
27
|
+
|
28
|
+
### OSX
|
29
|
+
|
30
|
+
|
31
|
+
#### C++ compiler
|
32
|
+
Check whether you have the xcode command line tools installed by typing `xcode-select -p`. If this command returns something like `/Applications/Xcode.app/Contents/Developer` then you have the command line tools installed already.
|
33
|
+
|
34
|
+
If you do not already have the xcode commandline tools installed running `xcode-select --install` will start you off down the correct path.
|
35
|
+
|
36
|
+
-------------------
|
37
|
+
|
38
|
+
At this point, it may be convenient to install Homebrew.
|
39
|
+
|
40
|
+
#### Python
|
41
|
+
|
42
|
+
If you're using a recent Mac, you should already have Python 2.7 installed on your machine. You can check what version of Python you're running by typing `python --version` into your terminal. If you don't have a recent version of python (version 2.7 or greater) installed, you'll
|
43
|
+
|
44
|
+
#### `git`
|
45
|
+
|
46
|
+
If you have homebrew installed simply type `brew install git`
|
47
|
+
|
48
|
+
### Linux (we'll assume ubuntu or debian)
|
49
|
+
|
50
|
+
#### C++ Compiler
|
51
|
+
`sudo apt-get install build-essential`
|
52
|
+
#### `git`
|
53
|
+
`sudo apt-get install git`
|
54
|
+
#### FreeImage
|
55
|
+
`sudo apt-get install libfreeimage-dev`
|
56
|
+
|
57
|
+
### Getting PDFium's dependencies
|
58
|
+
|
59
|
+
If you have any trouble check [PDFium's build instructions](https://code.google.com/p/pdfium/wiki/Build) for the most up to date instructions.
|
60
|
+
|
61
|
+
|
62
|
+
|
63
|
+
### Getting the PDFium code
|
64
|
+
|
65
|
+
`git clone https://pdfium.googlesource.com/pdfium`
|
66
|
+
|
67
|
+
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require_relative 'setup'
|
2
|
+
require 'benchmark'
|
3
|
+
|
4
|
+
here = File.dirname(__FILE__)
|
5
|
+
path = File.join(here, '..', 'test', 'fixtures', 'uncharter.pdf')
|
6
|
+
doc = PDFShaver::Document.new(path)
|
7
|
+
out_dir = File.join(here, 'output', 'speed')
|
8
|
+
|
9
|
+
count = 10
|
10
|
+
Benchmark.bm do |test|
|
11
|
+
test.report("naive"){ count.times{ doc.pages.each{ |page| full_naive_render(page, out_dir) } } }
|
12
|
+
test.report("smart"){ count.times{ doc.pages.each{ |page| full_smart_render(page, out_dir) } } }
|
13
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require_relative 'setup'
|
2
|
+
|
3
|
+
some_number_of = ARGV.pop.to_i
|
4
|
+
some_number_of = 5 if some_number_of <= 0
|
5
|
+
puts "firing up #{some_number_of} forks"
|
6
|
+
some_number_of.times do |n|
|
7
|
+
fork do
|
8
|
+
here = File.dirname(__FILE__)
|
9
|
+
path = File.join(here, '..', 'test', 'fixtures', 'uncharter.pdf')
|
10
|
+
extract(path, n)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
=begin
|
15
|
+
Questions:
|
16
|
+
* What can/should trigger Ruby's GC?
|
17
|
+
* What's the stack size look like?
|
18
|
+
* Is Ruby accurately reporting the amount of memory allocated? (how do we compare?) no!
|
19
|
+
* Can we notify Ruby about memory allocated in C/C++? No! \weep
|
20
|
+
=end
|
data/bench/setup.rb
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
require_relative '../lib/pdfshaver'
|
2
|
+
require 'fileutils'
|
3
|
+
require 'pp'
|
4
|
+
|
5
|
+
def extract(doc_path, prefix=rand(10**10))
|
6
|
+
out_dir = File.join(".", "output", prefix.to_s)
|
7
|
+
FileUtils.mkdir_p(out_dir)
|
8
|
+
log = File.open(File.join(out_dir, "log.txt"), 'w')
|
9
|
+
log.sync = true
|
10
|
+
doc = PDFShaver::Document.new(doc_path)
|
11
|
+
doc.pages.each do |page|
|
12
|
+
log.puts("#{Time.now}: rendering page #{page.number}")
|
13
|
+
# shamelessly stolen from http://samsaffron.com/archive/2014/04/08/ruby-2-1-garbage-collection-ready-for-production
|
14
|
+
log.puts "RSS: #{`ps -eo rss,pid | grep #{Process.pid} | grep -v grep | awk '{ print $1; }'`}"
|
15
|
+
#GC.start
|
16
|
+
#log.puts(GC.stat)
|
17
|
+
easy_render(page, out_dir)
|
18
|
+
end
|
19
|
+
log.puts ("#{Time.now}: Done!")
|
20
|
+
end
|
21
|
+
|
22
|
+
# A method to test basic rendering
|
23
|
+
def easy_render(page, dir)
|
24
|
+
out_path = File.join(dir,"#{page.number}.gif")
|
25
|
+
page.render(out_path)
|
26
|
+
end
|
27
|
+
|
28
|
+
# A method for testing rendering a variety of pages
|
29
|
+
# but as it turns out rendering isn't the problem so
|
30
|
+
# this method isn't any heavier in memory usage than
|
31
|
+
# the easy render!
|
32
|
+
def full_naive_render(page, dir)
|
33
|
+
sizes = %w[1000x 700x 180x 60x75!]
|
34
|
+
sizes.each do |size_string|
|
35
|
+
dimensions = page.extract_dimensions_from_gm_geometry_string(size_string)
|
36
|
+
out_path = File.join(dir,"#{page.number}_#{size_string}.gif")
|
37
|
+
#puts out_path
|
38
|
+
page.render(out_path, dimensions)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def full_smart_render(p, dir)
|
43
|
+
p.with_data_loaded do |page|
|
44
|
+
sizes = %w[1000x 700x 180x 60x75!]
|
45
|
+
sizes.each do |size_string|
|
46
|
+
dimensions = page.extract_dimensions_from_gm_geometry_string(size_string)
|
47
|
+
out_path = File.join(dir,"#{page.number}_#{size_string}.gif")
|
48
|
+
#puts out_path
|
49
|
+
page.render(out_path, dimensions)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
@@ -75,10 +75,6 @@ VALUE document_allocate(VALUE rb_PDFShaver_Document) {
|
|
75
75
|
|
76
76
|
// Entry point for PDFShaver::Document's ruby initializer into C++ land
|
77
77
|
VALUE initialize_document_internals(int arg_count, VALUE* args, VALUE self) {
|
78
|
-
// Get the PDFShaver namespace and get the `Document` class inside it.
|
79
|
-
VALUE rb_PDFShaver = rb_const_get(rb_cObject, rb_intern("PDFShaver"));
|
80
|
-
VALUE rb_PDFShaver_Document = rb_const_get(rb_PDFShaver, rb_intern("Document"));
|
81
|
-
|
82
78
|
// use Ruby's argument scanner to pull out a required
|
83
79
|
// `path` argument and an optional `options` hash.
|
84
80
|
VALUE path, options;
|
@@ -88,7 +84,8 @@ VALUE initialize_document_internals(int arg_count, VALUE* args, VALUE self) {
|
|
88
84
|
// path should at this point be validated & known to exist.
|
89
85
|
Document* document;
|
90
86
|
Data_Get_Struct(self, Document, document);
|
91
|
-
int parse_status =
|
87
|
+
//int parse_status =
|
88
|
+
document->load(path);
|
92
89
|
//document_handle_parse_status(parse_status, path);
|
93
90
|
if (!document->isValid()) { rb_raise(rb_eArgError, "failed to open file (%" PRIsVALUE")", path); }
|
94
91
|
|
@@ -99,10 +96,10 @@ VALUE initialize_document_internals(int arg_count, VALUE* args, VALUE self) {
|
|
99
96
|
|
100
97
|
void document_handle_parse_status(int status, VALUE path) {
|
101
98
|
//printf("\nSTATUS: %d\n", status);
|
102
|
-
VALUE rb_PDFShaver = rb_const_get(rb_cObject, rb_intern("PDFShaver"));
|
103
|
-
VALUE rb_eEncryptionError = rb_const_get(rb_PDFShaver, rb_intern("EncryptionError"));
|
104
|
-
VALUE rb_eInvalidFormatError = rb_const_get(rb_PDFShaver, rb_intern("InvalidFormatError"));
|
105
|
-
VALUE rb_eMissingHandlerError = rb_const_get(rb_PDFShaver, rb_intern("MissingHandlerError"));
|
99
|
+
//VALUE rb_PDFShaver = rb_const_get(rb_cObject, rb_intern("PDFShaver"));
|
100
|
+
//VALUE rb_eEncryptionError = rb_const_get(rb_PDFShaver, rb_intern("EncryptionError"));
|
101
|
+
//VALUE rb_eInvalidFormatError = rb_const_get(rb_PDFShaver, rb_intern("InvalidFormatError"));
|
102
|
+
//VALUE rb_eMissingHandlerError = rb_const_get(rb_PDFShaver, rb_intern("MissingHandlerError"));
|
106
103
|
|
107
104
|
//switch (status) {
|
108
105
|
// case PDFPARSE_ERROR_SUCCESS:
|
data/ext/pdfium_ruby/extconf.rb
CHANGED
@@ -2,9 +2,9 @@ require "mkmf"
|
|
2
2
|
require 'rbconfig'
|
3
3
|
# List directories to search for PDFium headers and library files to link against
|
4
4
|
def append_pdfium_directory_to paths
|
5
|
-
paths.map do |dir|
|
5
|
+
paths.map do |dir|
|
6
6
|
[
|
7
|
-
File.join(dir, 'pdfium'),
|
7
|
+
File.join(dir, 'pdfium'),
|
8
8
|
File.join(dir, 'pdfium', 'fpdfsdk', 'include'),
|
9
9
|
File.join(dir, 'pdfium', 'third_party', 'base', 'numerics')
|
10
10
|
]
|
@@ -56,13 +56,18 @@ LIB_FILES.each do | lib |
|
|
56
56
|
have_library(lib) or abort "Couldn't find library lib#{lib} in #{LIB_DIRS.join(', ')}"
|
57
57
|
end
|
58
58
|
|
59
|
-
$CPPFLAGS += " -fPIC -std=c++11"
|
59
|
+
$CPPFLAGS += " -fPIC -std=c++11 -Wall"
|
60
60
|
if RUBY_PLATFORM =~ /darwin/
|
61
61
|
have_library('objc')
|
62
62
|
FRAMEWORKS = %w{AppKit CoreFoundation}
|
63
63
|
$LDFLAGS << FRAMEWORKS.map { |f| " -framework #{f}" }.join
|
64
|
+
end
|
65
|
+
|
66
|
+
if ENV['DEBUG'] == '1'
|
67
|
+
$defs.push "-DDEBUG=1"
|
68
|
+
$CPPFLAGS += " -g"
|
64
69
|
else
|
65
|
-
|
70
|
+
$CPPFLAGS += " -O2"
|
66
71
|
end
|
67
72
|
|
68
73
|
create_makefile "pdfium_ruby"
|
data/ext/pdfium_ruby/page.cpp
CHANGED
@@ -5,22 +5,49 @@
|
|
5
5
|
* C++ Page definition
|
6
6
|
*********************************************/
|
7
7
|
|
8
|
+
// When created make sure C++ pages are marked as unopened.
|
8
9
|
Page::Page() { this->opened = false; }
|
9
10
|
|
10
|
-
|
11
|
+
// When destroying a C++ Page, make sure to dispose of the internals properly.
|
12
|
+
// And notify the parent document that this page is no longer going to be used.
|
13
|
+
Page::~Page() {
|
14
|
+
if (this->opened) {
|
15
|
+
this->unload();
|
16
|
+
this->document->notifyPageClosed(this);
|
17
|
+
}
|
18
|
+
}
|
19
|
+
|
20
|
+
// When the page is initialized through the Ruby lifecycle, store a reference
|
21
|
+
// to its parent Document, the page number and notify the Document that this page
|
22
|
+
// is available to be loaded.
|
23
|
+
void Page::initialize(Document* document, int page_index) {
|
11
24
|
this->document = document;
|
12
25
|
this->page_index = page_index;
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
26
|
+
this->document->notifyPageOpened(this);
|
27
|
+
}
|
28
|
+
|
29
|
+
// Load the page through PDFium and flag the document as currently open.
|
30
|
+
bool Page::load() {
|
31
|
+
if (!this->opened) {
|
32
|
+
this->fpdf_page = FPDF_LoadPage(this->document->fpdf_document, this->page_index);
|
33
|
+
this->opened = true;
|
34
|
+
}
|
17
35
|
return this->opened;
|
18
36
|
}
|
19
37
|
|
38
|
+
// Unload the page (freeing the page's memory) and mark it as not open.
|
39
|
+
void Page::unload() {
|
40
|
+
if (this->opened){ FPDF_ClosePage(this->fpdf_page); }
|
41
|
+
this->opened = false;
|
42
|
+
}
|
43
|
+
|
44
|
+
// readers for the page's dimensions.
|
20
45
|
double Page::width(){ return FPDF_GetPageWidth(this->fpdf_page); }
|
21
46
|
double Page::height(){ return FPDF_GetPageHeight(this->fpdf_page); }
|
22
47
|
double Page::aspect() { return width() / height(); }
|
23
48
|
|
49
|
+
// Render the page to a destination path with the dimensions
|
50
|
+
// specified by width & height (or appropriate defaults).
|
24
51
|
bool Page::render(char* path, int width, int height) {
|
25
52
|
// If no height or width is supplied, render at natural dimensions.
|
26
53
|
if (!width && !height) {
|
@@ -31,21 +58,16 @@ bool Page::render(char* path, int width, int height) {
|
|
31
58
|
// infer the other by preserving page aspect ratio.
|
32
59
|
if ( width && !height) { height = width / this->aspect(); }
|
33
60
|
if (!width && height) { width = height * this->aspect(); }
|
34
|
-
//printf("Derp? %d, %d\n", width, height);
|
35
61
|
|
36
62
|
// Create bitmap. width, height, alpha 1=enabled,0=disabled
|
37
63
|
bool alpha = false;
|
38
|
-
printf("just about to allocate bitmap w:%d, h:%d\n", width, height);
|
39
64
|
FPDF_BITMAP bitmap = FPDFBitmap_Create(width, height, alpha);
|
40
|
-
|
41
|
-
if (!bitmap) { printf("ALLOCATING BITMAP FAILED"); return false; }
|
65
|
+
if (!bitmap) { return false; }
|
42
66
|
|
43
|
-
|
44
|
-
// fill all pixels with white for the background color
|
67
|
+
// and fill all pixels with white for the background color
|
45
68
|
FPDFBitmap_FillRect(bitmap, 0, 0, width, height, 0xFFFFFFFF);
|
46
|
-
|
47
|
-
// Render a page
|
48
|
-
// args are: *buffer, page, start_x, start_y, size_x, size_y, rotation, and flags
|
69
|
+
|
70
|
+
// Render a page into the bitmap in RGBA format
|
49
71
|
// flags are:
|
50
72
|
// 0 for normal display, or combination of flags defined below
|
51
73
|
// 0x01 Set if annotations are to be rendered
|
@@ -56,7 +78,8 @@ bool Page::render(char* path, int width, int height) {
|
|
56
78
|
int rotation = 0;
|
57
79
|
int flags = FPDF_PRINTING; // A flag defined in PDFium's codebase.
|
58
80
|
FPDF_RenderPageBitmap(bitmap, this->fpdf_page, start_x, start_y, width, height, rotation, flags);
|
59
|
-
|
81
|
+
|
82
|
+
// Calculate the page's stride.
|
60
83
|
// The stride holds the width of one row in bytes. It may not be an exact
|
61
84
|
// multiple of the pixel width because the data may be packed to always end on a byte boundary
|
62
85
|
int stride = FPDFBitmap_GetStride(bitmap);
|
@@ -69,34 +92,30 @@ bool Page::render(char* path, int width, int height) {
|
|
69
92
|
((stride * height) > (INT_MAX / 3))
|
70
93
|
);
|
71
94
|
if (bitmapIsntValid){
|
72
|
-
printf("BITMAP ISN'T VALID");
|
73
95
|
FPDFBitmap_Destroy(bitmap);
|
74
96
|
return false;
|
75
97
|
}
|
76
98
|
|
77
|
-
//
|
99
|
+
// Hand off the PDFium bitmap data to FreeImage for additional processing.
|
78
100
|
unsigned bpp = 32;
|
79
101
|
unsigned red_mask = 0xFF0000;
|
80
102
|
unsigned green_mask = 0x00FF00;
|
81
103
|
unsigned blue_mask = 0x0000FF;
|
82
104
|
bool topdown = true;
|
83
105
|
FIBITMAP *raw = FreeImage_ConvertFromRawBits(
|
84
|
-
(BYTE*)FPDFBitmap_GetBuffer(bitmap), width, height, stride, bpp,
|
106
|
+
(BYTE*)FPDFBitmap_GetBuffer(bitmap), width, height, stride, bpp,
|
107
|
+
red_mask, green_mask, blue_mask, topdown);
|
85
108
|
|
86
|
-
|
87
|
-
// at this point we're done with the FPDF bitmap and can destroy it.
|
109
|
+
// With bitmap handoff complete the FPDF bitmap can be destroyed.
|
88
110
|
FPDFBitmap_Destroy(bitmap);
|
89
|
-
|
111
|
+
|
90
112
|
// Conversion to jpg or gif require that the bpp be set to 24
|
91
113
|
// since we're not exporting using alpha transparency above in FPDFBitmap_Create
|
92
114
|
FIBITMAP *image = FreeImage_ConvertTo24Bits(raw);
|
93
|
-
printf("CONVERT TO 24BITS2");
|
94
115
|
FreeImage_Unload(raw);
|
95
|
-
printf("DEALLOCATE RAW");
|
96
116
|
|
97
117
|
// figure out the desired format from the file extension
|
98
118
|
FREE_IMAGE_FORMAT format = FreeImage_GetFIFFromFilename(path);
|
99
|
-
printf("DEDUCE FORMAT");
|
100
119
|
|
101
120
|
bool success = false;
|
102
121
|
if ( FIF_GIF == format ){
|
@@ -108,19 +127,11 @@ bool Page::render(char* path, int width, int height) {
|
|
108
127
|
// All other formats should be just a save call
|
109
128
|
success = FreeImage_Save(format, image, path, 0);
|
110
129
|
}
|
111
|
-
printf("SAVED IMAGE");
|
112
130
|
|
113
131
|
// unload the image
|
114
132
|
FreeImage_Unload(image);
|
115
|
-
printf("UNLOADED IMAGE");
|
116
|
-
return success;
|
117
|
-
}
|
118
133
|
|
119
|
-
|
120
|
-
if (this->opened) {
|
121
|
-
FPDF_ClosePage(this->fpdf_page);
|
122
|
-
this->document->notifyPageClosed(this);
|
123
|
-
}
|
134
|
+
return success;
|
124
135
|
}
|
125
136
|
|
126
137
|
/********************************************
|
@@ -132,17 +143,64 @@ void Define_Page() {
|
|
132
143
|
VALUE rb_PDFShaver = rb_const_get(rb_cObject, rb_intern("PDFShaver"));
|
133
144
|
VALUE rb_PDFShaver_Page = rb_const_get(rb_PDFShaver, rb_intern("Page"));
|
134
145
|
|
146
|
+
// Define the C allocator function so that when a new PDFShaver::Page instance
|
147
|
+
// is created, our C/C++ data structures are initialized into the Ruby lifecycle.
|
135
148
|
rb_define_alloc_func(rb_PDFShaver_Page, *page_allocate);
|
136
149
|
|
137
|
-
|
150
|
+
// Wire the C functions we need/want into Ruby land.
|
151
|
+
// We're using the CPP_RUBY_METHOD_FUNC to wrap functions for C++'s comfort.
|
138
152
|
rb_define_private_method(rb_PDFShaver_Page, "initialize_page_internals",
|
139
153
|
CPP_RUBY_METHOD_FUNC(initialize_page_internals),-1);
|
154
|
+
rb_define_method(rb_PDFShaver_Page, "render", CPP_RUBY_METHOD_FUNC(page_render), -1);
|
155
|
+
rb_define_private_method(rb_PDFShaver_Page, "load_data", CPP_RUBY_METHOD_FUNC(page_load_data), 0);
|
156
|
+
rb_define_private_method(rb_PDFShaver_Page, "unload_data", CPP_RUBY_METHOD_FUNC(page_unload_data), 0);
|
140
157
|
}
|
141
158
|
|
159
|
+
// Create a new C++ Page object and store it in any newly created
|
160
|
+
// Ruby page instances.
|
142
161
|
VALUE page_allocate(VALUE rb_PDFShaver_Page) {
|
143
162
|
Page* page = new Page();
|
144
163
|
return Data_Wrap_Struct(rb_PDFShaver_Page, NULL, destroy_page, page);
|
145
164
|
}
|
165
|
+
// And delete the C++ page when we're done with the Ruby page.
|
166
|
+
static void destroy_page(Page* page) { delete page; }
|
167
|
+
|
168
|
+
// This function does the actual initialization of the C++ page's internals
|
169
|
+
// defining which page of the document will be opened when `load_data` is called.
|
170
|
+
VALUE initialize_page_internals(int arg_count, VALUE* args, VALUE self) {
|
171
|
+
// use Ruby's argument scanner to pull out a required
|
172
|
+
VALUE rb_document, page_index, options;
|
173
|
+
int number_of_args = rb_scan_args(arg_count, args, "21", &rb_document, &page_index, &options);
|
174
|
+
|
175
|
+
// fetch the C++ document from the Ruby document the page has been initialized with
|
176
|
+
Document* document;
|
177
|
+
Data_Get_Struct(rb_document, Document, document);
|
178
|
+
// And fetch the C++ page
|
179
|
+
Page* page;
|
180
|
+
Data_Get_Struct(self, Page, page);
|
181
|
+
// and associate them by initializing the C++ page.
|
182
|
+
page->initialize(document, FIX2INT(page_index));
|
183
|
+
return self;
|
184
|
+
}
|
185
|
+
|
186
|
+
VALUE page_load_data(VALUE self) {
|
187
|
+
Page* page;
|
188
|
+
Data_Get_Struct(self, Page, page);
|
189
|
+
if (! page->load() ) { rb_raise(rb_eRuntimeError, "Unable to load page data"); }
|
190
|
+
rb_ivar_set(self, rb_intern("@extension_data_is_loaded"), Qtrue);
|
191
|
+
rb_ivar_set(self, rb_intern("@width"), INT2FIX(page->width()));
|
192
|
+
rb_ivar_set(self, rb_intern("@height"), INT2FIX(page->height()));
|
193
|
+
rb_ivar_set(self, rb_intern("@aspect"), rb_float_new(page->aspect()));
|
194
|
+
return Qtrue;
|
195
|
+
}
|
196
|
+
|
197
|
+
VALUE page_unload_data(VALUE self) {
|
198
|
+
Page* page;
|
199
|
+
Data_Get_Struct(self, Page, page);
|
200
|
+
page->unload();
|
201
|
+
rb_ivar_set(self, rb_intern("@extension_data_is_loaded"), Qfalse);
|
202
|
+
return Qtrue;
|
203
|
+
}
|
146
204
|
|
147
205
|
//bool page_render(int arg_count, VALUE* args, VALUE self) {
|
148
206
|
VALUE page_render(int arg_count, VALUE* args, VALUE self) {
|
@@ -169,31 +227,8 @@ VALUE page_render(int arg_count, VALUE* args, VALUE self) {
|
|
169
227
|
|
170
228
|
Page* page;
|
171
229
|
Data_Get_Struct(self, Page, page);
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
VALUE rb_document, page_index, options;
|
178
|
-
int number_of_args = rb_scan_args(arg_count, args, "21", &rb_document, &page_index, &options);
|
179
|
-
|
180
|
-
// Get the PDFShaver namespace and get the `Page` class inside it.
|
181
|
-
VALUE rb_PDFShaver = rb_const_get(rb_cObject, rb_intern("PDFShaver"));
|
182
|
-
VALUE rb_PDFShaver_Page = rb_const_get(rb_PDFShaver, rb_intern("Page"));
|
183
|
-
|
184
|
-
Document* document;
|
185
|
-
Data_Get_Struct(rb_document, Document, document);
|
186
|
-
|
187
|
-
Page* page;
|
188
|
-
Data_Get_Struct(self, Page, page);
|
189
|
-
|
190
|
-
page->load(document, FIX2INT(page_index));
|
191
|
-
|
192
|
-
rb_ivar_set(self, rb_intern("@width"), INT2FIX(page->width()));
|
193
|
-
rb_ivar_set(self, rb_intern("@height"), INT2FIX(page->height()));
|
194
|
-
rb_ivar_set(self, rb_intern("@aspect"), rb_float_new(page->aspect()));
|
195
|
-
|
196
|
-
return self;
|
197
|
-
}
|
198
|
-
|
199
|
-
static void destroy_page(Page* page) { delete page; }
|
230
|
+
page_load_data(self);
|
231
|
+
VALUE output = (page->render(StringValuePtr(path), width, height) ? Qtrue : Qfalse);
|
232
|
+
page_unload_data(self);
|
233
|
+
return output;
|
234
|
+
}
|
data/ext/pdfium_ruby/page.h
CHANGED
@@ -10,7 +10,9 @@ class Page {
|
|
10
10
|
public:
|
11
11
|
Page();
|
12
12
|
|
13
|
-
|
13
|
+
void initialize(Document* document, int page_number);
|
14
|
+
bool load();
|
15
|
+
void unload();
|
14
16
|
|
15
17
|
double width();
|
16
18
|
double height();
|
@@ -31,6 +33,8 @@ void Define_Page();
|
|
31
33
|
VALUE initialize_page_internals(int arg_count, VALUE* args, VALUE self);
|
32
34
|
VALUE page_render(int arg_count, VALUE* args, VALUE self);
|
33
35
|
VALUE page_allocate(VALUE rb_PDFShaver_Page);
|
36
|
+
VALUE page_load_data(VALUE rb_PDFShaver_Page);
|
37
|
+
VALUE page_unload_data(VALUE rb_PDFShaver_Page);
|
34
38
|
static void destroy_page(Page* page);
|
35
39
|
|
36
40
|
#endif
|
data/lib/pdfshaver/page.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
module PDFShaver
|
2
2
|
class Page
|
3
3
|
GM_MATCHER = /^\s*((?<width>\d+)x((?<height>\d+))?|x?(?<height>\d+))(?<modifier>[@%!<>^]+)?\s*$/
|
4
|
-
attr_reader :document, :
|
4
|
+
attr_reader :document, :number, :index
|
5
5
|
|
6
6
|
def initialize document, number, options={}
|
7
7
|
raise ArgumentError unless document.kind_of? PDFShaver::Document
|
@@ -11,6 +11,7 @@ module PDFShaver
|
|
11
11
|
@number = number
|
12
12
|
@index = number - 1
|
13
13
|
@document = document
|
14
|
+
@extension_data_is_loaded = false
|
14
15
|
initialize_page_internals document, @index
|
15
16
|
end
|
16
17
|
|
@@ -24,6 +25,40 @@ module PDFShaver
|
|
24
25
|
self.index <=> other.index
|
25
26
|
end
|
26
27
|
|
28
|
+
def height
|
29
|
+
load_dimensions unless @height
|
30
|
+
@height
|
31
|
+
end
|
32
|
+
|
33
|
+
def width
|
34
|
+
load_dimensions unless @width
|
35
|
+
@width
|
36
|
+
end
|
37
|
+
|
38
|
+
def aspect
|
39
|
+
load_dimensions unless @aspect
|
40
|
+
@aspect
|
41
|
+
end
|
42
|
+
|
43
|
+
def with_data_loaded &block
|
44
|
+
load_data
|
45
|
+
output = yield self
|
46
|
+
unload_data
|
47
|
+
output
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
def load_dimensions
|
52
|
+
with_data_loaded do
|
53
|
+
# don't have to do anything, because loading/unloading page data
|
54
|
+
# will populate our dimensions.
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
public
|
59
|
+
# This code was written with the GraphicsMagick geometry argument parser
|
60
|
+
# as a direct reference. Its intent is to provide a compatibility layer
|
61
|
+
# for specifying page geometry that functions identically to graphicsmagick's.
|
27
62
|
def extract_dimensions_from_gm_geometry_string(arg)
|
28
63
|
dimensions = {}
|
29
64
|
return dimensions if arg.nil? or arg.empty?
|
@@ -44,6 +79,8 @@ module PDFShaver
|
|
44
79
|
current_area = self.width * self.height
|
45
80
|
target_area = (requested_width || 1) * (requested_height || 1)
|
46
81
|
|
82
|
+
# if upper or lower bounds are supplied
|
83
|
+
# check whether the target_area size adheres to that constraint.
|
47
84
|
resize = if modifier.include? '>'
|
48
85
|
current_area > target_area
|
49
86
|
elsif modifier.include? '<'
|
@@ -52,6 +89,7 @@ module PDFShaver
|
|
52
89
|
true
|
53
90
|
end
|
54
91
|
|
92
|
+
# Calculate page dimensions based on area
|
55
93
|
if resize
|
56
94
|
scale = 1.0 / Math.sqrt(current_area/target_area)
|
57
95
|
dimensions[:width] = (self.width*scale+0.25).floor
|
@@ -69,8 +107,8 @@ module PDFShaver
|
|
69
107
|
width = (self.width.to_f/self.height*height+0.5).floor
|
70
108
|
end
|
71
109
|
|
72
|
-
#
|
73
|
-
#
|
110
|
+
# For proportional mode, scales are specified by percent.
|
111
|
+
# Sizes are recalculated and stored as the target width in place for further processing
|
74
112
|
if modifier.include? '%'
|
75
113
|
x_scale = width
|
76
114
|
y_scale = height
|
data/pdfshaver.gemspec
CHANGED
@@ -4,26 +4,24 @@ require 'pdfshaver/version'
|
|
4
4
|
|
5
5
|
Gem::Specification.new do |s|
|
6
6
|
s.name = 'pdfshaver'
|
7
|
-
s.version = PDFShaver::VERSION
|
7
|
+
s.version = PDFShaver::VERSION
|
8
8
|
s.licenses = ['MIT']
|
9
9
|
s.summary = "Shave pages off of PDFs as images"
|
10
|
+
s.description = <<-DESCRIPTION
|
11
|
+
Shave pages off of PDFs as images. PDFShaver makes iterating PDF pages easy
|
12
|
+
by wrapping Google Chrome's PDFium library in an enumerable interface.
|
13
|
+
DESCRIPTION
|
14
|
+
s.homepage = 'https://www.documentcloud.org/opensource'
|
10
15
|
s.authors = ["Ted Han", "Nathan Stitt"]
|
11
16
|
s.email = 'opensource@documentcloud.org'
|
12
17
|
s.extensions = 'ext/pdfium_ruby/extconf.rb'
|
13
|
-
s.files =
|
14
|
-
|
15
|
-
|
16
|
-
ext/**/*
|
17
|
-
test/**/*
|
18
|
-
Gemfile
|
19
|
-
pdfshaver.gemspec
|
20
|
-
Rakefile
|
21
|
-
Readme.md
|
22
|
-
]
|
18
|
+
s.files = `git ls-files -z`.split("\x0")
|
19
|
+
s.test_files = s.files.grep(%r{^(test|spec|features)/})
|
20
|
+
s.require_paths = ["lib"]
|
23
21
|
|
24
|
-
s.add_development_dependency "bundler",
|
25
|
-
s.add_development_dependency 'rake'
|
26
|
-
s.add_development_dependency 'rake-compiler'
|
27
|
-
s.add_development_dependency 'minitest'
|
28
|
-
s.add_development_dependency 'fastimage'
|
22
|
+
s.add_development_dependency "bundler", "~> 1.5"
|
23
|
+
s.add_development_dependency 'rake', "~>10.4"
|
24
|
+
s.add_development_dependency 'rake-compiler', "~>0.9"
|
25
|
+
s.add_development_dependency 'minitest', "~>5.5"
|
26
|
+
s.add_development_dependency 'fastimage', "~>1.6"
|
29
27
|
end
|
data/test/gc_spec.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
require File.expand_path(File.join(File.dirname(__FILE__),'spec_helper'))
|
2
|
+
|
3
|
+
describe GC do
|
4
|
+
it "won't segfault if when a document is GCed" do
|
5
|
+
doc = PDFShaver::Document.new(File.join(FIXTURES,'uncharter.pdf'))
|
6
|
+
doc = nil
|
7
|
+
GC.start
|
8
|
+
end
|
9
|
+
|
10
|
+
it "won't segfault if when an invalid document is GCed" do
|
11
|
+
Proc.new{ PDFShaver::Document.new("suede shoes") }.must_raise ArgumentError
|
12
|
+
GC.start
|
13
|
+
end
|
14
|
+
|
15
|
+
it "won't segfault if document falls out of scope before pages" do
|
16
|
+
doc = PDFShaver::Document.new(File.join(FIXTURES,'uncharter.pdf'))
|
17
|
+
p1 = PDFShaver::Page.new(doc, 1)
|
18
|
+
doc = nil
|
19
|
+
GC.start
|
20
|
+
p1 = nil
|
21
|
+
GC.start
|
22
|
+
end
|
23
|
+
end
|
@@ -81,8 +81,8 @@ describe "Resize arguments" do
|
|
81
81
|
"200x200@" => Size.new(176, 227),
|
82
82
|
"1000>" => base,
|
83
83
|
#"1000<" => Size.new(773, 1000),
|
84
|
-
"500>" => Size.new(
|
85
|
-
"500x>" => Size.new(500,
|
84
|
+
"500>" => Size.new(390, 500),
|
85
|
+
"500x>" => Size.new(500, 640)
|
86
86
|
}.each do |input, expected|
|
87
87
|
#puts "#{input} : #{expected.inspect}"
|
88
88
|
output = @page.extract_dimensions_from_gm_geometry_string(input)
|
data/test/page_spec.rb
CHANGED
@@ -109,25 +109,49 @@ describe PDFShaver::Page do
|
|
109
109
|
end
|
110
110
|
end
|
111
111
|
|
112
|
-
describe "
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
112
|
+
describe "lazy loading" do
|
113
|
+
before do
|
114
|
+
@page = PDFShaver::Page.new(@document, 1)
|
115
|
+
@output_path = File.join OUTPUT, 'image_render_test.gif'
|
116
|
+
end
|
117
|
+
|
118
|
+
it "should be safe to reuse pages" do
|
119
|
+
@page.instance_variable_get("@extension_data_is_loaded").must_equal false
|
120
|
+
@page.render(@output_path)
|
121
|
+
@page.instance_variable_get("@extension_data_is_loaded").must_equal false
|
122
|
+
@page.render(@output_path)
|
123
|
+
@page.instance_variable_get("@extension_data_is_loaded").must_equal false
|
117
124
|
end
|
118
125
|
|
119
|
-
it "
|
120
|
-
|
121
|
-
|
126
|
+
it "should not load data until requested" do
|
127
|
+
@page.instance_variable_get("@extension_data_is_loaded").must_equal false
|
128
|
+
@page.instance_variable_get("@height").must_equal nil
|
129
|
+
@page.instance_variable_get("@width").must_equal nil
|
130
|
+
@page.instance_variable_get("@aspect").must_equal nil
|
131
|
+
|
132
|
+
@page.instance_variable_get("@extension_data_is_loaded").must_equal false
|
133
|
+
@page.send(:load_dimensions)
|
134
|
+
@page.instance_variable_get("@extension_data_is_loaded").must_equal false
|
135
|
+
@page.height.wont_equal nil
|
136
|
+
@page.width.wont_equal nil
|
137
|
+
@page.aspect.wont_equal nil
|
138
|
+
@page.instance_variable_get("@extension_data_is_loaded").must_equal false
|
122
139
|
end
|
123
140
|
|
124
|
-
it "
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
141
|
+
it "should provide a scope where data is kept loaded" do
|
142
|
+
@page.instance_variable_get("@extension_data_is_loaded").must_equal false
|
143
|
+
@page.with_data_loaded do
|
144
|
+
@page.instance_variable_get("@extension_data_is_loaded").must_equal true
|
145
|
+
end
|
146
|
+
@page.instance_variable_get("@extension_data_is_loaded").must_equal false
|
147
|
+
end
|
148
|
+
|
149
|
+
it "shouldn't blow up if nested twice" do
|
150
|
+
@page.with_data_loaded do |p|
|
151
|
+
p.with_data_loaded do |lol|
|
152
|
+
lol
|
153
|
+
end
|
154
|
+
end
|
131
155
|
end
|
132
156
|
end
|
133
157
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: pdfshaver
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.1
|
4
|
+
version: 0.0.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ted Han
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2015-02-
|
12
|
+
date: 2015-02-27 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: bundler
|
@@ -29,68 +29,74 @@ dependencies:
|
|
29
29
|
name: rake
|
30
30
|
requirement: !ruby/object:Gem::Requirement
|
31
31
|
requirements:
|
32
|
-
- - "
|
32
|
+
- - "~>"
|
33
33
|
- !ruby/object:Gem::Version
|
34
|
-
version: '
|
34
|
+
version: '10.4'
|
35
35
|
type: :development
|
36
36
|
prerelease: false
|
37
37
|
version_requirements: !ruby/object:Gem::Requirement
|
38
38
|
requirements:
|
39
|
-
- - "
|
39
|
+
- - "~>"
|
40
40
|
- !ruby/object:Gem::Version
|
41
|
-
version: '
|
41
|
+
version: '10.4'
|
42
42
|
- !ruby/object:Gem::Dependency
|
43
43
|
name: rake-compiler
|
44
44
|
requirement: !ruby/object:Gem::Requirement
|
45
45
|
requirements:
|
46
|
-
- - "
|
46
|
+
- - "~>"
|
47
47
|
- !ruby/object:Gem::Version
|
48
|
-
version: '0'
|
48
|
+
version: '0.9'
|
49
49
|
type: :development
|
50
50
|
prerelease: false
|
51
51
|
version_requirements: !ruby/object:Gem::Requirement
|
52
52
|
requirements:
|
53
|
-
- - "
|
53
|
+
- - "~>"
|
54
54
|
- !ruby/object:Gem::Version
|
55
|
-
version: '0'
|
55
|
+
version: '0.9'
|
56
56
|
- !ruby/object:Gem::Dependency
|
57
57
|
name: minitest
|
58
58
|
requirement: !ruby/object:Gem::Requirement
|
59
59
|
requirements:
|
60
|
-
- - "
|
60
|
+
- - "~>"
|
61
61
|
- !ruby/object:Gem::Version
|
62
|
-
version: '
|
62
|
+
version: '5.5'
|
63
63
|
type: :development
|
64
64
|
prerelease: false
|
65
65
|
version_requirements: !ruby/object:Gem::Requirement
|
66
66
|
requirements:
|
67
|
-
- - "
|
67
|
+
- - "~>"
|
68
68
|
- !ruby/object:Gem::Version
|
69
|
-
version: '
|
69
|
+
version: '5.5'
|
70
70
|
- !ruby/object:Gem::Dependency
|
71
71
|
name: fastimage
|
72
72
|
requirement: !ruby/object:Gem::Requirement
|
73
73
|
requirements:
|
74
|
-
- - "
|
74
|
+
- - "~>"
|
75
75
|
- !ruby/object:Gem::Version
|
76
|
-
version: '
|
76
|
+
version: '1.6'
|
77
77
|
type: :development
|
78
78
|
prerelease: false
|
79
79
|
version_requirements: !ruby/object:Gem::Requirement
|
80
80
|
requirements:
|
81
|
-
- - "
|
81
|
+
- - "~>"
|
82
82
|
- !ruby/object:Gem::Version
|
83
|
-
version: '
|
84
|
-
description:
|
83
|
+
version: '1.6'
|
84
|
+
description: " Shave pages off of PDFs as images. PDFShaver makes iterating PDF
|
85
|
+
pages easy \n by wrapping Google Chrome's PDFium library in an enumerable interface.\n"
|
85
86
|
email: opensource@documentcloud.org
|
86
87
|
executables: []
|
87
88
|
extensions:
|
88
89
|
- ext/pdfium_ruby/extconf.rb
|
89
90
|
extra_rdoc_files: []
|
90
91
|
files:
|
92
|
+
- ".gitignore"
|
91
93
|
- Gemfile
|
94
|
+
- Gemfile.lock
|
92
95
|
- Rakefile
|
93
96
|
- Readme.md
|
97
|
+
- bench/data_loading_speed.rb
|
98
|
+
- bench/memory_stress.rb
|
99
|
+
- bench/setup.rb
|
94
100
|
- ext/pdfium_ruby/document.cpp
|
95
101
|
- ext/pdfium_ruby/document.h
|
96
102
|
- ext/pdfium_ruby/extconf.rb
|
@@ -105,15 +111,14 @@ files:
|
|
105
111
|
- lib/pdfshaver/version.rb
|
106
112
|
- pdfshaver.gemspec
|
107
113
|
- test/document_spec.rb
|
108
|
-
- test/fixtures/completely_encrypted.pdf
|
109
|
-
- test/fixtures/encrypted.pdf
|
110
114
|
- test/fixtures/letter-to-canadians-from-jack-layton.pdf
|
111
115
|
- test/fixtures/uncharter.pdf
|
116
|
+
- test/gc_spec.rb
|
112
117
|
- test/gm_compatability_spec.rb
|
113
118
|
- test/page_set_spec.rb
|
114
119
|
- test/page_spec.rb
|
115
120
|
- test/spec_helper.rb
|
116
|
-
homepage:
|
121
|
+
homepage: https://www.documentcloud.org/opensource
|
117
122
|
licenses:
|
118
123
|
- MIT
|
119
124
|
metadata: {}
|
@@ -128,13 +133,21 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
128
133
|
version: '0'
|
129
134
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
130
135
|
requirements:
|
131
|
-
- - "
|
136
|
+
- - ">="
|
132
137
|
- !ruby/object:Gem::Version
|
133
|
-
version:
|
138
|
+
version: '0'
|
134
139
|
requirements: []
|
135
140
|
rubyforge_project:
|
136
141
|
rubygems_version: 2.4.5
|
137
142
|
signing_key:
|
138
143
|
specification_version: 4
|
139
144
|
summary: Shave pages off of PDFs as images
|
140
|
-
test_files:
|
145
|
+
test_files:
|
146
|
+
- test/document_spec.rb
|
147
|
+
- test/fixtures/letter-to-canadians-from-jack-layton.pdf
|
148
|
+
- test/fixtures/uncharter.pdf
|
149
|
+
- test/gc_spec.rb
|
150
|
+
- test/gm_compatability_spec.rb
|
151
|
+
- test/page_set_spec.rb
|
152
|
+
- test/page_spec.rb
|
153
|
+
- test/spec_helper.rb
|
Binary file
|
data/test/fixtures/encrypted.pdf
DELETED
Binary file
|