caracal 1.5.0 → 1.6.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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/push_gem.yml +29 -0
  3. data/CHANGELOG.md +22 -0
  4. data/README.md +72 -4
  5. data/caracal.gemspec +9 -3
  6. data/lib/caracal/core/footer.rb +50 -0
  7. data/lib/caracal/core/header.rb +49 -0
  8. data/lib/caracal/core/models/field_model.rb +131 -0
  9. data/lib/caracal/core/models/footer_model.rb +43 -0
  10. data/lib/caracal/core/models/header_model.rb +43 -0
  11. data/lib/caracal/core/models/iframe_model.rb +39 -4
  12. data/lib/caracal/core/models/image_model.rb +1 -1
  13. data/lib/caracal/core/models/list_item_model.rb +57 -3
  14. data/lib/caracal/core/models/list_model.rb +1 -7
  15. data/lib/caracal/core/models/list_style_model.rb +1 -1
  16. data/lib/caracal/core/models/page_number_model.rb +17 -2
  17. data/lib/caracal/core/models/paragraph_model.rb +16 -1
  18. data/lib/caracal/core/models/relationship_model.rb +1 -0
  19. data/lib/caracal/core/models/table_model.rb +1 -1
  20. data/lib/caracal/core/page_numbers.rb +5 -1
  21. data/lib/caracal/document.rb +98 -29
  22. data/lib/caracal/renderers/content_types_renderer.rb +3 -1
  23. data/lib/caracal/renderers/document_renderer.rb +47 -10
  24. data/lib/caracal/renderers/footer_renderer.rb +57 -50
  25. data/lib/caracal/renderers/header_footer_renderer.rb +90 -0
  26. data/lib/caracal/renderers/header_renderer.rb +28 -0
  27. data/lib/caracal/renderers/relationships_renderer.rb +22 -4
  28. data/lib/caracal/version.rb +1 -1
  29. data/lib/caracal.rb +16 -0
  30. data/spec/lib/caracal/core/footer_spec.rb +37 -0
  31. data/spec/lib/caracal/core/header_spec.rb +48 -0
  32. data/spec/lib/caracal/core/images_spec.rb +7 -0
  33. data/spec/lib/caracal/core/models/footer_model_spec.rb +41 -0
  34. data/spec/lib/caracal/core/models/header_model_spec.rb +41 -0
  35. data/spec/lib/caracal/core/models/image_model_spec.rb +1 -1
  36. data/spec/lib/caracal/core/models/list_item_model_spec.rb +18 -0
  37. data/spec/lib/caracal/core/models/list_style_model_spec.rb +1 -1
  38. data/spec/lib/caracal/core/models/option_keys_spec.rb +47 -0
  39. data/spec/lib/caracal/core/models/page_number_model_spec.rb +16 -1
  40. data/spec/lib/caracal/core/models/paragraph_model_spec.rb +22 -1
  41. data/spec/lib/caracal/core/models/table_model_spec.rb +1 -1
  42. data/spec/lib/caracal/core/relationships_spec.rb +1 -1
  43. data/spec/lib/caracal/document_output_spec.rb +503 -0
  44. metadata +35 -13
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3357d1a61526623b3acb0ead4e0b990ce1c70026ea321ba014415947380daec9
4
- data.tar.gz: 5be1b9e7bec559c1199c2c17befe1238c3ab522c1ef1fdb3a49a58e3b102c67f
3
+ metadata.gz: c06f9a2d1fafe9fc3cbc91db5bab3d08a374ef0b688ff188a200e175f0c72073
4
+ data.tar.gz: 7dd9194ecb1a0d84e9e5953a334f0fc5754fbd696305e8fa21a8563f95c2a3fa
5
5
  SHA512:
6
- metadata.gz: ad771cd941a199997acf1138bd1d426e5f2a4d1d5feb2065366a85de09a5f31295f9eee3a97b7264732e27b09706c722e4532f0a2164f73c00d3b3543623eddc
7
- data.tar.gz: d89fe871c0f5569292adfd8ec6a5c044bf1096ecb95e711b2c55a9a4aa1324317d757f5ab8057d20582ff0ccd3c5b6f150c5b5a25a3a4aed81634a495ce32ad2
6
+ metadata.gz: 56a868f230d4ae1ad7ab96f1e001aca3efac537026cf3e23f66936e18f8779e692fccc52c1118a55b25a5750e8aeda51e7e347c16b37264c598682cc9aba0748
7
+ data.tar.gz: '02099d9534ed1c0d950028f7dc937e47394082d2f05a4909f554a68a052f1d58f8ed84be30a55f2e91a15e240dc805b65b2e42ac1104b6989965b797ad573f98'
@@ -0,0 +1,29 @@
1
+ name: Push gem
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ jobs:
9
+ push:
10
+ runs-on: ubuntu-latest
11
+
12
+ permissions:
13
+ contents: write
14
+ id-token: write
15
+
16
+ environment: release
17
+
18
+ steps:
19
+ - uses: actions/checkout@v5
20
+ with:
21
+ persist-credentials: false
22
+
23
+ - name: Set up Ruby
24
+ uses: ruby/setup-ruby@v1
25
+ with:
26
+ bundler-cache: true
27
+ ruby-version: ruby
28
+
29
+ - uses: rubygems/release-gem@v1
data/CHANGELOG.md CHANGED
@@ -1,3 +1,25 @@
1
+ #### v1.6.0
2
+
3
+ * Enhancements
4
+ * Added `header` and `footer`, which accept the same content as the document body: paragraphs, images, tables and links. Images and links resolve through each part's own relationships. (@benjaminketron, @jamesridgway, @dlauer)
5
+ * Added the `field` paragraph method, which inserts a `:page` or `:numpages` field, so a footer can read "Page 2 of 7". (@benjaminketron, @jamesridgway, @dlauer)
6
+ * Added a different first page: `header first: true` and `footer first: true` give the first page its own header or footer, or leave it blank when called without a block, and `page_numbers first_page: false` leaves the number off the first page. (@dlauer)
7
+ * Added the `start` option to `page_numbers`, which sets the number of the first page. (@dlauer)
8
+ * `img` now accepts `data:` and `ppi:` as options, not only inside its block. Previously the options were silently dropped and the image was read from the path instead. (@victorpolko, @dlauer)
9
+ * `keep_next`, `header_rows`, the list style `restart` and the page number `size` are likewise accepted as options. (@dlauer)
10
+ * Entries read from documents embedded with `iframe` are capped by `IFrameModel.max_entry_size` (50MB by default), so a small upload cannot expand into an arbitrarily large allocation. (@dlauer)
11
+ * Raised the nokogiri floor to 1.16.2 and added source, changelog and issue tracker links to the gem metadata. (@dlauer)
12
+
13
+ * Changes
14
+ * `footer` and `page_numbers` now combine, with the page number rendered below the footer content. Previously `page_numbers` replaced the footer.
15
+ * `header` and `footer` raise `InvalidModelError` when given an empty block.
16
+
17
+ * Bug Fixes
18
+ * Page numbers are rendered as a complete complex field, so viewers that do not evaluate fields, such as WPS Office, show the number rather than "PAGE". (@dlauer)
19
+ * Text added to a list item after a nested list now appears after that list, aligned with the item's text, rather than before it. An item can also hold more than one nested list. (@dlauer)
20
+ * Documents written with rubyzip 3 no longer carry zip64 records, which LibreOffice 7.4 and earlier refuse to open. (@dlauer)
21
+
22
+
1
23
  #### v1.5.0
2
24
 
3
25
  * Enhancements
data/README.md CHANGED
@@ -321,14 +321,61 @@ Page numbers can be added to the footer via the `page_numbers` method. The meth
321
321
  docx.page_numbers true do
322
322
  align :right # sets the alignment. accepts :left, :center, and :right.
323
323
  label 'Page' # sets the text that will go to the left of the page number. Defaults to nil.
324
- size: 24 # sets the label and number size simultaneously. units in half points.
325
- label_size: 24 # sets the label size only. units in half points.
326
- number_size: 20 # sets the number size only. units in half points.
324
+ size 24 # sets the label and number size simultaneously. units in half points.
325
+ label_size 24 # sets the label size only. units in half points.
326
+ number_size 20 # sets the number size only. units in half points.
327
327
  end
328
328
  ```
329
329
 
330
330
  The `size` option and the `label_size` and `number_size` options are mutually exclusive.
331
331
 
332
+ The `start` option sets the number of the first page, which is useful when the document continues another one, or has a cover page you don't want counted. It also applies to `field :page` in a custom footer, so it can be set without showing the built-in page number:
333
+
334
+ ```ruby
335
+ docx.page_numbers true, start: 0 # the cover page is page 0, so the next page is page 1
336
+ docx.page_numbers start: 12 # number from 12 without the built-in page number
337
+ ```
338
+
339
+ ### Headers and Footers
340
+
341
+ A header or footer that appears on every page can be added with the `header` and `footer` methods. Their blocks accept the same paragraph, list, table, rule and image methods as the document body.
342
+
343
+ ```ruby
344
+ docx.header do
345
+ img 'logo.png', width: 120, height: 32, align: :right
346
+ p 'Quarterly Report', align: :center
347
+ end
348
+
349
+ docx.footer do
350
+ p align: :center do
351
+ text 'Page '
352
+ field :page
353
+ text ' of '
354
+ field :numpages
355
+ end
356
+ end
357
+ ```
358
+
359
+ If you also turn on `page_numbers`, the page number is rendered below the footer content.
360
+
361
+ *Headers and footers must contain content. Calling `header` or `footer` with an empty block raises an `InvalidModelError`.*
362
+
363
+ #### Different First Page
364
+
365
+ Pass `first: true` to give the first page its own header or footer, such as a cover page. Without a block, it leaves the first page's header or footer blank. Use `first_page: false` to leave the page number off the first page.
366
+
367
+ ```ruby
368
+ docx.header first: true do # replaces the header on the first page only
369
+ p 'Annual Report 2026', align: :center, size: 36
370
+ end
371
+
372
+ docx.header first: true # no header on the first page
373
+ docx.footer first: true # no footer content on the first page
374
+
375
+ docx.page_numbers true, first_page: false # no page number on the first page
376
+ ```
377
+
378
+ When the first page has its own header or footer, any part you don't set for it falls back to the default. So `first_page: false` on its own hides only the page number, and the default header and footer content still appear.
332
379
 
333
380
  ### Fonts
334
381
 
@@ -446,6 +493,16 @@ docx.p do
446
493
  end
447
494
  ```
448
495
 
496
+ Fields that Word fills in can be added with the `field` method. It accepts `:page` (the current page number) and `:numpages` (the total number of pages), along with the same formatting options as `text`.
497
+
498
+ ```ruby
499
+ docx.p do
500
+ text 'Page '
501
+ field :page, bold: true
502
+ text ' of '
503
+ field :numpages
504
+ end
505
+ ```
449
506
 
450
507
  ### Links
451
508
 
@@ -575,7 +632,7 @@ docx.ul do
575
632
  end
576
633
  ```
577
634
 
578
- Lists can nested as many levels deep as you wish and mixed in any combination.
635
+ Lists can be nested as many levels deep as you wish and mixed in any combination. An item can contain more than one nested list. Text that follows a nested list renders after it, as an unnumbered paragraph aligned with the item's text.
579
636
 
580
637
  ```ruby
581
638
  docx.ul do
@@ -624,6 +681,7 @@ Images can be added by using the `img` method. The method accepts several optio
624
681
  ```ruby
625
682
  docx.img 'https://www.example.com/logo.png' do
626
683
  data raw_data # sets the file data directly instead of opening the url
684
+ ppi 72 # sets the pixels per inch used to convert width and height. defaults to 72.
627
685
  width 396 # sets the image width. units specified in pixels.
628
686
  height 216 # sets the image height. units specified in pixels.
629
687
  align :right # controls the justification of the image. default is :left.
@@ -634,6 +692,16 @@ docx.img 'https://www.example.com/logo.png' do
634
692
  end
635
693
  ```
636
694
 
695
+ Every setting can also be passed as an option instead of in a block:
696
+
697
+ ```ruby
698
+ docx.img 'logo.png', data: File.binread('path/to/logo.png'), width: 396, height: 216
699
+ ```
700
+
701
+ *Note: `data` must be the raw bytes of the image, not Base64. Caracal writes it
702
+ into the document unchanged, so Base64-encoded data produces a corrupt image.
703
+ Read files with `File.binread` so line endings are not translated on Windows.*
704
+
637
705
  *Note: If you provide the image data, you should still supply a URL. I know this
638
706
  is a bit hacky, but it allows the library to key the image more effectively and
639
707
  Caracal needs a file extension to apply to the renamed media file. This seemed
data/caracal.gemspec CHANGED
@@ -6,13 +6,19 @@ require 'caracal/version'
6
6
  Gem::Specification.new do |spec|
7
7
  spec.name = 'caracal'
8
8
  spec.version = Caracal::VERSION
9
- spec.authors = ['Urvin LLC', 'John Dugan', 'Dave Lauer']
10
- spec.email = ['jpdugan@gmail.com', 'dave@plia.com']
9
+ spec.authors = ['Urvin LLC', 'Dave Lauer']
10
+ spec.email = ['dave@plia.com']
11
11
  spec.summary = %q{ Fast, professional Microsoft Word (docx) writer for Ruby. }
12
12
  spec.description = %q{ Caracal is a pure Ruby Microsoft Word generation library that produces professional quality MSWord documents (docx) using a simple, HTML-style DSL. }
13
13
  spec.homepage = 'https://github.com/urvin-compliance/caracal'
14
14
  spec.license = 'MIT'
15
15
 
16
+ spec.metadata = {
17
+ 'source_code_uri' => 'https://github.com/urvin-compliance/caracal',
18
+ 'changelog_uri' => 'https://github.com/urvin-compliance/caracal/blob/master/CHANGELOG.md',
19
+ 'bug_tracker_uri' => 'https://github.com/urvin-compliance/caracal/issues'
20
+ }
21
+
16
22
  spec.files = `git ls-files -z`.split("\x0")
17
23
  spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
24
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
@@ -20,7 +26,7 @@ Gem::Specification.new do |spec|
20
26
 
21
27
  spec.required_ruby_version = '>= 3.1'
22
28
 
23
- spec.add_dependency 'nokogiri', '~> 1.6'
29
+ spec.add_dependency 'nokogiri', '>= 1.16.2', '< 2.0'
24
30
  spec.add_dependency 'rubyzip', '>= 1.1.6', '< 4.0'
25
31
  spec.add_dependency 'tilt', '>= 1.4'
26
32
 
@@ -0,0 +1,50 @@
1
+ require 'caracal/core/models/footer_model'
2
+ require 'caracal/errors'
3
+
4
+
5
+ module Caracal
6
+ module Core
7
+
8
+ # This module encapsulates all the functionality related to adding a
9
+ # footer on every page of the document.
10
+ #
11
+ module Footer
12
+ def self.included(base)
13
+ base.class_eval do
14
+
15
+ #-------------------------------------------------------------
16
+ # Public Methods
17
+ #-------------------------------------------------------------
18
+
19
+ # With `first: true` the footer replaces the default one on the
20
+ # first page only. Without a block it leaves the first page's
21
+ # footer blank.
22
+ #
23
+ def footer(*args, &block)
24
+ options = Caracal::Utilities.extract_options!(args)
25
+ first = options.delete(:first)
26
+
27
+ model = Caracal::Core::Models::FooterModel.new(options, &block)
28
+ if first
29
+ @first_footer_content = model
30
+ elsif model.valid?
31
+ @footer_content = model
32
+ else
33
+ raise Caracal::Errors::InvalidModelError, 'footer must contain at least one paragraph, list, table, rule or image.'
34
+ end
35
+ model
36
+ end
37
+
38
+ def footer_content
39
+ @footer_content
40
+ end
41
+
42
+ def first_footer_content
43
+ @first_footer_content
44
+ end
45
+
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,49 @@
1
+ require 'caracal/core/models/header_model'
2
+ require 'caracal/errors'
3
+
4
+
5
+ module Caracal
6
+ module Core
7
+
8
+ # This module encapsulates all the functionality related to adding a header
9
+ # to every page of the document.
10
+ #
11
+ module Header
12
+ def self.included(base)
13
+ base.class_eval do
14
+
15
+ #-------------------------------------------------------------
16
+ # Public Methods
17
+ #-------------------------------------------------------------
18
+
19
+ # With `first: true` the header replaces the default one on the
20
+ # first page only. Without a block it leaves the first page's
21
+ # header blank.
22
+ #
23
+ def header(*args, &block)
24
+ options = Caracal::Utilities.extract_options!(args)
25
+ first = options.delete(:first)
26
+
27
+ model = Caracal::Core::Models::HeaderModel.new(options, &block)
28
+ if first
29
+ @first_header_content = model
30
+ elsif model.valid?
31
+ @header_content = model
32
+ else
33
+ raise Caracal::Errors::InvalidModelError, 'header must contain at least one paragraph, list, table, rule or image.'
34
+ end
35
+ model
36
+ end
37
+
38
+ def header_content
39
+ @header_content
40
+ end
41
+
42
+ def first_header_content
43
+ @first_header_content
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,131 @@
1
+ require 'caracal/core/models/base_model'
2
+
3
+
4
+ module Caracal
5
+ module Core
6
+ module Models
7
+
8
+ # This class encapsulates the logic needed to store and render a
9
+ # field, such as the current page number, inside a paragraph.
10
+ #
11
+ class FieldModel < BaseModel
12
+
13
+ #--------------------------------------------------
14
+ # Configuration
15
+ #--------------------------------------------------
16
+
17
+ # constants
18
+ const_set(:TYPE_MAP, { page: 'PAGE', numpages: 'NUMPAGES' })
19
+
20
+ # accessors
21
+ attr_reader :field_type
22
+ attr_reader :field_style
23
+ attr_reader :field_font
24
+ attr_reader :field_color
25
+ attr_reader :field_size
26
+ attr_reader :field_bold
27
+ attr_reader :field_italic
28
+ attr_reader :field_underline
29
+ attr_reader :field_bgcolor
30
+ attr_reader :field_highlight_color
31
+ attr_reader :field_vertical_align
32
+
33
+ #-------------------------------------------------------------
34
+ # Public Class Methods
35
+ #-------------------------------------------------------------
36
+
37
+ def self.formatted_type(type)
38
+ TYPE_MAP.fetch(type.to_s.to_sym)
39
+ end
40
+
41
+
42
+ #-------------------------------------------------------------
43
+ # Public Instance Methods
44
+ #-------------------------------------------------------------
45
+
46
+ #========== GETTERS ===============================
47
+
48
+ def formatted_type
49
+ self.class.formatted_type(field_type)
50
+ end
51
+
52
+ # .run_attributes
53
+ def run_attributes
54
+ {
55
+ style: field_style,
56
+ font: field_font,
57
+ color: field_color,
58
+ size: field_size,
59
+ bold: field_bold,
60
+ italic: field_italic,
61
+ underline: field_underline,
62
+ bgcolor: field_bgcolor,
63
+ highlight_color: field_highlight_color,
64
+ vertical_align: field_vertical_align
65
+ }
66
+ end
67
+
68
+
69
+ #========== SETTERS ===============================
70
+
71
+ # booleans
72
+ [:bold, :italic, :underline].each do |m|
73
+ define_method "#{ m }" do |value|
74
+ instance_variable_set("@field_#{ m }", !!value)
75
+ end
76
+ end
77
+
78
+ # integers
79
+ [:size].each do |m|
80
+ define_method "#{ m }" do |value|
81
+ instance_variable_set("@field_#{ m }", value.to_i)
82
+ end
83
+ end
84
+
85
+ # strings
86
+ [:bgcolor, :color, :font, :highlight_color, :style, :type].each do |m|
87
+ define_method "#{ m }" do |value|
88
+ instance_variable_set("@field_#{ m }", value.to_s)
89
+ end
90
+ end
91
+
92
+ # symbols
93
+ [:vertical_align].each do |m|
94
+ define_method "#{ m }" do |value|
95
+ instance_variable_set("@field_#{ m }", value.to_s.to_sym)
96
+ end
97
+ end
98
+
99
+
100
+ #========== VALIDATION ============================
101
+
102
+ def valid?
103
+ TYPE_MAP.key?(field_type.to_s.to_sym)
104
+ end
105
+
106
+
107
+ #--------------------------------------------------
108
+ # Private Methods
109
+ #--------------------------------------------------
110
+ private
111
+
112
+ def option_keys
113
+ [:type, :style, :font, :color, :size, :bold, :italic, :underline, :bgcolor, :highlight_color, :vertical_align]
114
+ end
115
+
116
+ def method_missing(method, *args, &block)
117
+ # I'm on the fence with respect to this implementation. We're ignoring
118
+ # :method_missing errors to allow syntax flexibility for paragraph-type
119
+ # models. The issue is the syntax format of those models--the way we pass
120
+ # the content value as a special argument--coupled with the model's
121
+ # ability to accept nested instructions.
122
+ #
123
+ # By ignoring method missing errors here, we can pass the entire paragraph
124
+ # block in the initial, built-in call to :text.
125
+ end
126
+
127
+ end
128
+
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,43 @@
1
+ require 'caracal/core/models/base_model'
2
+
3
+
4
+ module Caracal
5
+ module Core
6
+ module Models
7
+
8
+ # This class holds the contents of the footer that appears on
9
+ # every page of the document.
10
+ #
11
+ class FooterModel < BaseModel
12
+
13
+ #-------------------------------------------------------------
14
+ # Public Methods
15
+ #-------------------------------------------------------------
16
+
17
+ #=============== DATA ACCESSORS =======================
18
+
19
+ def contents
20
+ @contents ||= []
21
+ end
22
+
23
+ # Images and links in the footer are registered with the document,
24
+ # which numbers them and writes the media, but Word resolves them
25
+ # against the footer part's own relationships. The renderer collects
26
+ # them here so they can be written to that part.
27
+ #
28
+ def relationships
29
+ @relationships ||= []
30
+ end
31
+
32
+
33
+ #=============== VALIDATION ===========================
34
+
35
+ def valid?
36
+ contents.size > 0
37
+ end
38
+
39
+ end
40
+
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,43 @@
1
+ require 'caracal/core/models/base_model'
2
+
3
+
4
+ module Caracal
5
+ module Core
6
+ module Models
7
+
8
+ # This class holds the contents of the header that appears on
9
+ # every page of the document.
10
+ #
11
+ class HeaderModel < BaseModel
12
+
13
+ #-------------------------------------------------------------
14
+ # Public Methods
15
+ #-------------------------------------------------------------
16
+
17
+ #=============== DATA ACCESSORS =======================
18
+
19
+ def contents
20
+ @contents ||= []
21
+ end
22
+
23
+ # Images and links in the header are registered with the document,
24
+ # which numbers them and writes the media, but Word resolves them
25
+ # against the header part's own relationships. The renderer collects
26
+ # them here so they can be written to that part.
27
+ #
28
+ def relationships
29
+ @relationships ||= []
30
+ end
31
+
32
+
33
+ #=============== VALIDATION ===========================
34
+
35
+ def valid?
36
+ contents.size > 0
37
+ end
38
+
39
+ end
40
+
41
+ end
42
+ end
43
+ end
@@ -1,11 +1,12 @@
1
1
  require 'tempfile'
2
2
  require 'caracal/core/models/base_model'
3
+ require 'caracal/errors'
3
4
 
4
5
  module Caracal
5
6
  module Core
6
7
  module Models
7
8
 
8
- # This class handles block options passed to the img method.
9
+ # This class handles block options passed to the iframe method.
9
10
  #
10
11
  class IFrameModel < BaseModel
11
12
 
@@ -13,6 +14,9 @@ module Caracal
13
14
  # Configuration
14
15
  #--------------------------------------------------
15
16
 
17
+ # constants
18
+ const_set(:DEFAULT_MAX_ENTRY_SIZE, 50 * 1024 * 1024) # 50MB
19
+
16
20
  # accessors
17
21
  attr_reader :iframe_url
18
22
  attr_reader :iframe_data
@@ -26,6 +30,37 @@ module Caracal
26
30
  end
27
31
 
28
32
 
33
+ #--------------------------------------------------
34
+ # Class Methods
35
+ #--------------------------------------------------
36
+
37
+ # The largest single entry Caracal will read out of an embedded
38
+ # document. Zip entries expand to many times their stored size, so
39
+ # without a limit a small file can exhaust memory. Set to nil to
40
+ # disable the check.
41
+ #
42
+ def self.max_entry_size
43
+ defined?(@max_entry_size) ? @max_entry_size : DEFAULT_MAX_ENTRY_SIZE
44
+ end
45
+
46
+ def self.max_entry_size=(value)
47
+ @max_entry_size = value
48
+ end
49
+
50
+ # This method reads a single entry from an embedded document,
51
+ # refusing anything larger than :max_entry_size.
52
+ #
53
+ def self.read_entry(entry)
54
+ limit = max_entry_size
55
+
56
+ if limit && entry.size > limit
57
+ raise Caracal::Errors::InvalidModelError, "iframe entry #{ entry.name } is #{ entry.size } bytes, over the #{ limit } byte limit."
58
+ end
59
+
60
+ entry.get_input_stream.read
61
+ end
62
+
63
+
29
64
  #--------------------------------------------------
30
65
  # Public Methods
31
66
  #--------------------------------------------------
@@ -36,12 +71,12 @@ module Caracal
36
71
  ::Zip::File.open(file) do |zip|
37
72
  # locate relationships xml
38
73
  entry = zip.glob('word/_rels/document.xml.rels').first
39
- content = entry.get_input_stream.read
74
+ content = self.class.read_entry(entry)
40
75
  rel_xml = Nokogiri::XML(content)
41
76
 
42
77
  # locate document xml
43
78
  entry = zip.glob('word/document.xml').first
44
- content = entry.get_input_stream.read
79
+ content = self.class.read_entry(entry)
45
80
  doc_xml = Nokogiri::XML(content)
46
81
 
47
82
  # master nodesets
@@ -75,7 +110,7 @@ module Caracal
75
110
  p_node = node.children[0].children[0]
76
111
  p_id = p_node.attributes['id'].to_s.to_i
77
112
  p_name = p_node.attributes['name'].to_s
78
- p_data = zip.glob(r_media).first.get_input_stream.read
113
+ p_data = self.class.read_entry(zip.glob(r_media).first)
79
114
 
80
115
  # register relationship
81
116
  array << { id: r_id, type: 'image', target: p_name, data: p_data }
@@ -115,7 +115,7 @@ module Caracal
115
115
  private
116
116
 
117
117
  def option_keys
118
- [:url, :width, :height, :align, :top, :bottom, :left, :right]
118
+ [:url, :data, :ppi, :width, :height, :align, :top, :bottom, :left, :right]
119
119
  end
120
120
 
121
121
  def pixels_to_emus(value, ppi)