caxlsx 4.3.0 → 4.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: c0f9a2b86a38ca45670bc0e74443d58a8d56b89a5d3d4891641bf6168fbcba02
4
- data.tar.gz: b8f16b2ae2e718fb07c763c4085da9acbc3982ab63bffcee3de5a9b5741e504c
3
+ metadata.gz: 97d26d1c2165080987473b430781c434b27f62510950ce0c27792058ab493bbf
4
+ data.tar.gz: 6d9f1f55b9b6602d86e2f6e30ce5e0589ac811bf49f8991f7dbc7f8fec42e6e3
5
5
  SHA512:
6
- metadata.gz: b2830801b587922e0e765d147e7eaa01d33527d82efc3950d4241889e828fabf814270c64d0f18c4dd91880eaa848cff3a7c4b7c3f4f38e1ba95ef004900208b
7
- data.tar.gz: f08afe03aedf885c94f035b69302bcd55a024efe4801c056a9383b47b2f5052d7fb4259b74bda32e82ecb6d2ece041e89573a26c233330991467d66344fc7689
6
+ metadata.gz: fd8f19deac0273ac8f59aca129190fc395b23077e34bfaafaa50dc48b9e6565430b94c7bbac8f19fd7272edacf0cac04438a2dc464b22670a575be75259cb2fe
7
+ data.tar.gz: 92ef32593a1ba1132b2b53ae08ecd82eb7e538edfdbb5e764fb325c57f6a2c66b56de6b67e04a48aa3dde5b663468a06d876292103f22fcf456b0da6563df967
data/CHANGELOG.md CHANGED
@@ -2,6 +2,15 @@ CHANGELOG
2
2
  ---------
3
3
  - **Unreleased**
4
4
 
5
+ - **September.01.25**: 4.4.0
6
+ - [PR #477](https://github.com/caxlsx/caxlsx/pull/477) Add package-level encryption and password protection.
7
+ - [PR #476](https://github.com/caxlsx/caxlsx/pull/476) Add Excel for Windows integration testing.
8
+ - [PR #476](https://github.com/caxlsx/caxlsx/pull/476) Add Excel for MacOS integration testing.
9
+ - [PR #469](https://github.com/caxlsx/caxlsx/pull/469) Add default theme file to Excel package.
10
+ - [PR #475](https://github.com/caxlsx/caxlsx/pull/475) Use timecop to fix transient time failure in tests
11
+ - [PR #474](https://github.com/caxlsx/caxlsx/pull/474) Add Windows and MacOS to the CI.
12
+ - [PR #474](https://github.com/caxlsx/caxlsx/pull/474) Fix local image file MIME type detection on Windows.
13
+ - [PR #474](https://github.com/caxlsx/caxlsx/pull/474) Load only HTTP headers when determining remote file MIME type.
5
14
 
6
15
  - **August.16.25**: 4.3.0
7
16
  - [PR #421](https://github.com/caxlsx/caxlsx/pull/421) Add Rubyzip >= 2.4 support
data/README.md CHANGED
@@ -51,20 +51,21 @@ cell level input data validation.
51
51
 
52
52
  15. Support for page margins and print options
53
53
 
54
- 16. Support for password and non password based sheet protection.
54
+ 16. Support for workbook-level encryption and password protection (requires [ooxml_crypt](https://github.com/teamsimplepay/ooxml_crypt) gem which only supports MRI Ruby.)
55
55
 
56
- 17. First stage interoperability support for GoogleDocs, LibreOffice,
57
- and Numbers
56
+ 17. Support for sheet-level password and non-password protection.
58
57
 
59
- 18. Support for defined names, which gives you repeated header rows for printing.
58
+ 18. First stage interoperability support for GoogleDocs, LibreOffice, and Numbers.
60
59
 
61
- 19. Data labels for charts as well as series color customization.
60
+ 19. Support for defined names, which gives you repeated header rows for printing.
62
61
 
63
- 20. Support for sheet headers and footers
62
+ 20. Data labels for charts as well as series color customization.
64
63
 
65
- 21. Pivot Tables
64
+ 21. Support for sheet headers and footers
66
65
 
67
- 22. Page Breaks
66
+ 22. Pivot Tables
67
+
68
+ 23. Page Breaks
68
69
 
69
70
 
70
71
  ## Install
@@ -127,6 +128,8 @@ Currently the following additional gems are available:
127
128
  * Provides a `.axlsx` renderer to Rails so you can move all your spreadsheet code from your controller into view files.
128
129
  - [activeadmin-caxlsx](https://github.com/caxlsx/activeadmin-caxlsx)
129
130
  * An Active Admin plugin that includes DSL to create downloadable reports.
131
+ - [ooxml_crypt](https://github.com/teamsimplepay/ooxml_crypt)
132
+ * Required to enable workbook encryption and password protection.
130
133
 
131
134
  ## Security
132
135
 
data/lib/axlsx/package.rb CHANGED
@@ -82,6 +82,8 @@ module Axlsx
82
82
  # @option options [String] :zip_command When `nil`, `#serialize` with RubyZip to
83
83
  # zip the XLSX file contents. When a String, the provided zip command (e.g.,
84
84
  # "zip") is used to zip the file contents (may be faster for large files)
85
+ # @option options [String] :password When specified, the serialized packaged will be
86
+ # encrypted with the password. Requires ooxml_crypt gem.
85
87
  # @return [Boolean] False if confirm_valid and validation errors exist. True if the package was serialized
86
88
  # @note A tremendous amount of effort has gone into ensuring that you cannot create invalid xlsx documents.
87
89
  # options[:confirm_valid] should be used in the rare case that you cannot open the serialized file.
@@ -108,7 +110,7 @@ module Axlsx
108
110
  workbook.apply_styles
109
111
  end
110
112
 
111
- confirm_valid, zip_command = parse_serialize_options(options, secondary_options)
113
+ confirm_valid, zip_command, password = parse_serialize_options(options, secondary_options)
112
114
  return false unless !confirm_valid || validate.empty?
113
115
 
114
116
  zip_provider = if zip_command
@@ -120,15 +122,31 @@ module Axlsx
120
122
  zip_provider.open(output) do |zip|
121
123
  write_parts(zip)
122
124
  end
125
+
126
+ if password && !password.empty?
127
+ require_ooxml_crypt!
128
+ OoxmlCrypt.encrypt_file(output, password, output)
129
+ end
130
+
123
131
  true
124
132
  ensure
125
133
  Relationship.clear_ids_cache
126
134
  end
127
135
 
128
136
  # Serialize your workbook to a StringIO instance
129
- # @param [Boolean] confirm_valid Validate the package prior to serialization.
130
- # @return [StringIO|Boolean] False if confirm_valid and validation errors exist. rewound string IO if not.
131
- def to_stream(confirm_valid = false)
137
+ # @param [Boolean] old_confirm_valid (Deprecated) Validate the package prior to serialization.
138
+ # Use :confirm_valid keyword arg instead.
139
+ # @option kwargs [Boolean] :confirm_valid Validate the package prior to serialization.
140
+ # @option kwargs [String] :password When specified, the serialized packaged will be
141
+ # encrypted with the password. Requires ooxml_crypt gem.
142
+ # @return [StringIO|Boolean] False if confirm_valid and validation errors exist. Rewound string IO if not.
143
+ def to_stream(old_confirm_valid = nil, confirm_valid: false, password: nil)
144
+ unless old_confirm_valid.nil?
145
+ warn "[DEPRECATION] Axlsx::Package#to_stream with confirm_valid as a non-keyword arg is deprecated. " \
146
+ "Use keyword arg instead e.g., package.to_stream(confirm_valid: false)"
147
+ confirm_valid ||= old_confirm_valid
148
+ end
149
+
132
150
  unless workbook.styles_applied
133
151
  workbook.apply_styles
134
152
  end
@@ -140,6 +158,12 @@ module Axlsx
140
158
  write_parts(zip)
141
159
  end
142
160
  stream.rewind
161
+
162
+ if password && !password.empty?
163
+ require_ooxml_crypt!
164
+ stream = StringIO.new(OoxmlCrypt.encrypt(stream.read, password))
165
+ end
166
+
143
167
  stream
144
168
  ensure
145
169
  Relationship.clear_ids_cache
@@ -221,6 +245,7 @@ module Axlsx
221
245
  def parts
222
246
  parts = [
223
247
  { entry: "xl/#{STYLES_PN}", doc: workbook.styles, schema: SML_XSD },
248
+ { entry: "xl/#{THEME_PN}", doc: workbook.theme, schema: THEME_XSD },
224
249
  { entry: CORE_PN, doc: @core, schema: CORE_XSD },
225
250
  { entry: APP_PN, doc: @app, schema: APP_XSD },
226
251
  { entry: WORKBOOK_RELS_PN, doc: workbook.relationships, schema: RELS_XSD },
@@ -357,6 +382,7 @@ module Axlsx
357
382
  c_types << Override.new(PartName: "/#{APP_PN}", ContentType: APP_CT)
358
383
  c_types << Override.new(PartName: "/#{CORE_PN}", ContentType: CORE_CT)
359
384
  c_types << Override.new(PartName: "/xl/#{STYLES_PN}", ContentType: STYLES_CT)
385
+ c_types << Override.new(PartName: "/xl/#{THEME_PN}", ContentType: THEME_CT)
360
386
  c_types << Axlsx::Override.new(PartName: "/#{WORKBOOK_PN}", ContentType: WORKBOOK_CT)
361
387
  c_types.lock
362
388
  c_types
@@ -375,8 +401,8 @@ module Axlsx
375
401
  end
376
402
 
377
403
  # Parse the arguments of `#serialize`
378
- # @return [Boolean, (String or nil)] Returns an array where the first value is
379
- # `confirm_valid` and the second is the `zip_command`.
404
+ # @return [Boolean, (String or nil), (String or nil)] Returns a 3-tuple where values are
405
+ # `confirm_valid`, `zip_command`, and `password`.
380
406
  # @private
381
407
  def parse_serialize_options(options, secondary_options)
382
408
  if secondary_options
@@ -385,17 +411,23 @@ module Axlsx
385
411
  end
386
412
  if options.is_a?(Hash)
387
413
  options.merge!(secondary_options || {})
388
- invalid_keys = options.keys - [:confirm_valid, :zip_command]
414
+ invalid_keys = options.keys - [:confirm_valid, :zip_command, :password]
389
415
  if invalid_keys.any?
390
416
  raise ArgumentError, "Invalid keyword arguments: #{invalid_keys}"
391
417
  end
392
418
 
393
- [options.fetch(:confirm_valid, false), options.fetch(:zip_command, nil)]
419
+ [options.fetch(:confirm_valid, false), options.fetch(:zip_command, nil), options.fetch(:password, nil)]
394
420
  else
395
421
  warn "[DEPRECATION] Axlsx::Package#serialize with confirm_valid as a boolean is deprecated. " \
396
422
  "Use keyword args instead e.g., package.serialize(output, confirm_valid: false)"
397
423
  parse_serialize_options((secondary_options || {}).merge(confirm_valid: options), nil)
398
424
  end
399
425
  end
426
+
427
+ def require_ooxml_crypt!
428
+ return if defined?(OoxmlCrypt)
429
+
430
+ raise 'Axlsx encryption requires ooxml_crypt gem'
431
+ end
400
432
  end
401
433
  end
@@ -15,6 +15,7 @@ module Axlsx
15
15
  require_relative 'table_style'
16
16
  require_relative 'table_styles'
17
17
  require_relative 'table_style_element'
18
+ require_relative 'theme'
18
19
  require_relative 'dxf'
19
20
  require_relative 'xf'
20
21
  require_relative 'cell_protection'
@@ -0,0 +1,163 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Axlsx
4
+ # Theme represents the theme part of the package and is responsible for
5
+ # generating the default Office theme that is required for encryption compatibility
6
+ class Theme
7
+ # The part name of this theme
8
+ # @return [String]
9
+ def pn
10
+ THEME_PN
11
+ end
12
+
13
+ # Serializes the default theme to XML
14
+ # @param [String] str
15
+ # @return [String]
16
+ def to_xml_string(str = +'')
17
+ str << <<~XML.delete("\n")
18
+ <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
19
+ <a:theme xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" name="Office Theme">
20
+ <a:themeElements>
21
+ <a:clrScheme name="Office">
22
+ <a:dk1><a:sysClr val="windowText" lastClr="000000"/></a:dk1>
23
+ <a:lt1><a:sysClr val="window" lastClr="FFFFFF"/></a:lt1>
24
+ <a:dk2><a:srgbClr val="1F497D"/></a:dk2>
25
+ <a:lt2><a:srgbClr val="EEECE1"/></a:lt2>
26
+ <a:accent1><a:srgbClr val="4F81BD"/></a:accent1>
27
+ <a:accent2><a:srgbClr val="C0504D"/></a:accent2>
28
+ <a:accent3><a:srgbClr val="9BBB59"/></a:accent3>
29
+ <a:accent4><a:srgbClr val="8064A2"/></a:accent4>
30
+ <a:accent5><a:srgbClr val="4BACC6"/></a:accent5>
31
+ <a:accent6><a:srgbClr val="F79646"/></a:accent6>
32
+ <a:hlink><a:srgbClr val="0000FF"/></a:hlink>
33
+ <a:folHlink><a:srgbClr val="800080"/></a:folHlink>
34
+ </a:clrScheme>
35
+
36
+ <a:fontScheme name="Office">
37
+ <a:majorFont>
38
+ <a:latin typeface="Cambria"/>
39
+ <a:ea typeface=""/>
40
+ <a:cs typeface=""/>
41
+ </a:majorFont>
42
+ <a:minorFont>
43
+ <a:latin typeface="Calibri"/>
44
+ <a:ea typeface=""/>
45
+ <a:cs typeface=""/>
46
+ </a:minorFont>
47
+ </a:fontScheme>
48
+
49
+ <a:fmtScheme name="Office">
50
+ <a:fillStyleLst>
51
+ <a:solidFill><a:schemeClr val="phClr"/></a:solidFill>
52
+ <a:gradFill rotWithShape="1">
53
+ <a:gsLst>
54
+ <a:gs pos="0"><a:schemeClr val="phClr"><a:tint val="50000"/><a:satMod val="300000"/></a:schemeClr></a:gs>
55
+ <a:gs pos="35000"><a:schemeClr val="phClr"><a:tint val="37000"/><a:satMod val="300000"/></a:schemeClr></a:gs>
56
+ <a:gs pos="100000"><a:schemeClr val="phClr"><a:tint val="15000"/><a:satMod val="350000"/></a:schemeClr></a:gs>
57
+ </a:gsLst>
58
+ <a:lin ang="16200000" scaled="1"/>
59
+ </a:gradFill>
60
+ <a:gradFill rotWithShape="1">
61
+ <a:gsLst>
62
+ <a:gs pos="0"><a:schemeClr val="phClr"><a:tint val="100000"/><a:shade val="100000"/><a:satMod val="130000"/></a:schemeClr></a:gs>
63
+ <a:gs pos="100000"><a:schemeClr val="phClr"><a:tint val="50000"/><a:shade val="100000"/><a:satMod val="350000"/></a:schemeClr></a:gs>
64
+ </a:gsLst>
65
+ <a:lin ang="16200000" scaled="false"/>
66
+ </a:gradFill>
67
+ </a:fillStyleLst>
68
+
69
+ <a:lnStyleLst>
70
+ <a:ln w="9525" cap="flat" cmpd="sng" algn="ctr">
71
+ <a:solidFill><a:schemeClr val="phClr"><a:shade val="95000"/><a:satMod val="105000"/></a:schemeClr></a:solidFill>
72
+ <a:prstDash val="solid"/>
73
+ </a:ln>
74
+ <a:ln w="25400" cap="flat" cmpd="sng" algn="ctr">
75
+ <a:solidFill><a:schemeClr val="phClr"/></a:solidFill>
76
+ <a:prstDash val="solid"/>
77
+ </a:ln>
78
+ <a:ln w="38100" cap="flat" cmpd="sng" algn="ctr">
79
+ <a:solidFill><a:schemeClr val="phClr"/></a:solidFill>
80
+ <a:prstDash val="solid"/>
81
+ </a:ln>
82
+ </a:lnStyleLst>
83
+
84
+ <a:effectStyleLst>
85
+ <a:effectStyle>
86
+ <a:effectLst>
87
+ <a:outerShdw blurRad="40000" dist="20000" dir="5400000" rotWithShape="false">
88
+ <a:srgbClr val="000000"><a:alpha val="38000"/></a:srgbClr>
89
+ </a:outerShdw>
90
+ </a:effectLst>
91
+ </a:effectStyle>
92
+ <a:effectStyle>
93
+ <a:effectLst>
94
+ <a:outerShdw blurRad="40000" dist="23000" dir="5400000" rotWithShape="false">
95
+ <a:srgbClr val="000000"><a:alpha val="35000"/></a:srgbClr>
96
+ </a:outerShdw>
97
+ </a:effectLst>
98
+ </a:effectStyle>
99
+ <a:effectStyle>
100
+ <a:effectLst>
101
+ <a:outerShdw blurRad="40000" dist="23000" dir="5400000" rotWithShape="false">
102
+ <a:srgbClr val="000000"><a:alpha val="35000"/></a:srgbClr>
103
+ </a:outerShdw>
104
+ </a:effectLst>
105
+ <a:scene3d>
106
+ <a:camera prst="orthographicFront"><a:rot lat="0" lon="0" rev="0"/></a:camera>
107
+ <a:lightRig rig="threePt" dir="t"><a:rot lat="0" lon="0" rev="1200000"/></a:lightRig>
108
+ </a:scene3d>
109
+ <a:sp3d><a:bevelT w="63500" h="25400"/></a:sp3d>
110
+ </a:effectStyle>
111
+ </a:effectStyleLst>
112
+
113
+ <a:bgFillStyleLst>
114
+ <a:solidFill><a:schemeClr val="phClr"/></a:solidFill>
115
+ <a:gradFill rotWithShape="1">
116
+ <a:gsLst>
117
+ <a:gs pos="0"><a:schemeClr val="phClr"><a:tint val="40000"/><a:satMod val="350000"/></a:schemeClr></a:gs>
118
+ <a:gs pos="40000"><a:schemeClr val="phClr"><a:tint val="45000"/><a:shade val="99000"/><a:satMod val="350000"/></a:schemeClr></a:gs>
119
+ <a:gs pos="100000"><a:schemeClr val="phClr"><a:shade val="20000"/><a:satMod val="255000"/></a:schemeClr></a:gs>
120
+ </a:gsLst>
121
+ <a:path path="circle"><a:fillToRect l="50000" t="-80000" r="50000" b="180000"/></a:path>
122
+ </a:gradFill>
123
+ <a:gradFill rotWithShape="1">
124
+ <a:gsLst>
125
+ <a:gs pos="0"><a:schemeClr val="phClr"><a:tint val="80000"/><a:satMod val="300000"/></a:schemeClr></a:gs>
126
+ <a:gs pos="100000"><a:schemeClr val="phClr"><a:shade val="30000"/><a:satMod val="200000"/></a:schemeClr></a:gs>
127
+ </a:gsLst>
128
+ <a:path path="circle"><a:fillToRect l="50000" t="50000" r="50000" b="50000"/></a:path>
129
+ </a:gradFill>
130
+ </a:bgFillStyleLst>
131
+ </a:fmtScheme>
132
+ </a:themeElements>
133
+
134
+ <a:objectDefaults>
135
+ <a:spDef>
136
+ <a:spPr/>
137
+ <a:bodyPr/>
138
+ <a:lstStyle/>
139
+ <a:style>
140
+ <a:lnRef idx="1"><a:schemeClr val="accent1"/></a:lnRef>
141
+ <a:fillRef idx="3"><a:schemeClr val="accent1"/></a:fillRef>
142
+ <a:effectRef idx="2"><a:schemeClr val="accent1"/></a:effectRef>
143
+ <a:fontRef idx="minor"><a:schemeClr val="lt1"/></a:fontRef>
144
+ </a:style>
145
+ </a:spDef>
146
+ <a:lnDef>
147
+ <a:spPr/>
148
+ <a:bodyPr/>
149
+ <a:lstStyle/>
150
+ <a:style>
151
+ <a:lnRef idx="2"><a:schemeClr val="accent1"/></a:lnRef>
152
+ <a:fillRef idx="0"><a:schemeClr val="accent1"/></a:fillRef>
153
+ <a:effectRef idx="1"><a:schemeClr val="accent1"/></a:effectRef>
154
+ <a:fontRef idx="minor"><a:schemeClr val="tx1"/></a:fontRef>
155
+ </a:style>
156
+ </a:lnDef>
157
+ </a:objectDefaults>
158
+ <a:extraClrSchemeLst/>
159
+ </a:theme>
160
+ XML
161
+ end
162
+ end
163
+ end
@@ -79,6 +79,9 @@ module Axlsx
79
79
  # shared strings namespace
80
80
  SHARED_STRINGS_R = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings"
81
81
 
82
+ # theme rels namespace
83
+ THEME_R = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme"
84
+
82
85
  # drawing rels namespace
83
86
  DRAWING_R = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing"
84
87
 
@@ -133,6 +136,9 @@ module Axlsx
133
136
  # shared strings content type
134
137
  SHARED_STRINGS_CT = "application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml"
135
138
 
139
+ # theme content type
140
+ THEME_CT = "application/vnd.openxmlformats-officedocument.theme+xml"
141
+
136
142
  # core content type
137
143
  CORE_CT = "application/vnd.openxmlformats-package.core-properties+xml"
138
144
 
@@ -187,6 +193,9 @@ module Axlsx
187
193
  # shared_strings part
188
194
  SHARED_STRINGS_PN = "sharedStrings.xml"
189
195
 
196
+ # theme part
197
+ THEME_PN = "theme/theme1.xml"
198
+
190
199
  # app part
191
200
  APP_PN = "docProps/app.xml"
192
201
 
@@ -259,6 +268,9 @@ module Axlsx
259
268
  # drawing validation schema
260
269
  DRAWING_XSD = "#{SCHEMA_BASE}dml-spreadsheetDrawing.xsd"
261
270
 
271
+ # theme validation schema
272
+ THEME_XSD = "#{SCHEMA_BASE}dml-main.xsd"
273
+
262
274
  # number format id for percentage formatting using the default formatting id.
263
275
  NUM_FMT_PERCENT = 9
264
276
 
@@ -1,22 +1,81 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'open-uri'
4
-
5
3
  module Axlsx
6
4
  # This module defines some utils related with mime type detection
7
5
  module MimeTypeUtils
8
- # Detect a file mime type
9
- # @param [String] v File path
10
- # @return [String] File mime type
11
- def self.get_mime_type(v)
12
- Marcel::MimeType.for(Pathname.new(v))
13
- end
6
+ # Extension to MIME type mapping for Windows fallback
7
+ EXTENSION_FALLBACKS = {
8
+ '.jpg' => 'image/jpeg',
9
+ '.jpeg' => 'image/jpeg',
10
+ '.png' => 'image/png',
11
+ '.gif' => 'image/gif'
12
+ }.freeze
13
+
14
+ class << self
15
+ # Detect a file mime type
16
+ # @param [String] v File path
17
+ # @return [String] File mime type
18
+ def get_mime_type(v)
19
+ mime_type = Marcel::MimeType.for(Pathname.new(v))
20
+
21
+ # Windows fallback: Marcel sometimes returns application/octet-stream for valid image files
22
+ if mime_type == 'application/octet-stream' && windows_platform?
23
+ extension = File.extname(v).downcase
24
+ # Verify it's actually an image by checking the file header
25
+ if EXTENSION_FALLBACKS.key?(extension) && File.exist?(v) && valid_image_file?(v, extension)
26
+ mime_type = EXTENSION_FALLBACKS[extension]
27
+ end
28
+ end
29
+
30
+ mime_type
31
+ end
32
+
33
+ # Detect a file mime type from URI
34
+ # @param [String] v URI
35
+ # @return [String] File mime type
36
+ def get_mime_type_from_uri(v)
37
+ uri = URI.parse(v)
38
+
39
+ unless uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
40
+ raise URI::InvalidURIError, "Only HTTP/HTTPS URIs are supported"
41
+ end
42
+
43
+ response = UriUtils.fetch_headers(uri)
44
+ mime_type = response&.[]('content-type')&.split(';')&.first&.strip
45
+
46
+ if mime_type.nil? || mime_type.empty?
47
+ raise ArgumentError, "Unable to determine MIME type from response"
48
+ end
49
+
50
+ mime_type
51
+ end
52
+
53
+ private
54
+
55
+ def windows_platform?
56
+ RUBY_PLATFORM =~ /mswin|mingw|cygwin/
57
+ end
58
+
59
+ def valid_image_file?(file_path, extension)
60
+ return false unless File.exist?(file_path)
14
61
 
15
- # Detect a file mime type from URI
16
- # @param [String] v URI
17
- # @return [String] File mime type
18
- def self.get_mime_type_from_uri(v)
19
- Marcel::MimeType.for(URI.parse(v).open)
62
+ # Check magic bytes for common image formats
63
+ begin
64
+ header = File.binread(file_path, 10)
65
+ case extension
66
+ when '.jpg', '.jpeg'
67
+ header[0..2].bytes == [0xFF, 0xD8, 0xFF] # JPEG magic bytes
68
+ when '.png'
69
+ header[0..7].bytes == [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A] # PNG magic bytes
70
+ when '.gif'
71
+ header[0..2] == 'GIF' # GIF magic bytes
72
+ else
73
+ false
74
+ end
75
+ rescue StandardError
76
+ false
77
+ end
78
+ end
20
79
  end
21
80
  end
22
81
  end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'uri'
5
+
6
+ module Axlsx
7
+ # This module defines some utils related to mime type detection
8
+ module UriUtils
9
+ class << self
10
+ # Performs an HTTP GeT request fetching only headers
11
+ def fetch_headers(uri, redirect_limit = 5)
12
+ redirect_count = 0
13
+ use_get = false
14
+
15
+ while redirect_count < redirect_limit
16
+ case (response = fetch_headers_request(uri, use_get: use_get))
17
+ when Net::HTTPSuccess
18
+ return response
19
+ when Net::HTTPMethodNotAllowed, Net::HTTPNotImplemented
20
+ fail_response(response) if use_get
21
+ use_get = true
22
+ next # Retry current URL with GET
23
+ when Net::HTTPRedirection
24
+ uri = follow_redirect(uri, response)
25
+ redirect_count += 1
26
+ else
27
+ fail_response(response)
28
+ end
29
+ end
30
+
31
+ raise ArgumentError, "Too many redirects (exceeded #{redirect_limit})"
32
+ end
33
+
34
+ private
35
+
36
+ def fetch_headers_request(uri, use_get: false)
37
+ Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
38
+ path = build_path(uri)
39
+ if use_get
40
+ http.request_get(path) { |response| break(response) } # Exit early with just headers
41
+ else
42
+ http.head(path)
43
+ end
44
+ end
45
+ end
46
+
47
+ def build_path(uri)
48
+ "#{uri.path.empty? ? '/' : uri.path}#{"?#{uri.query}" if uri.query}"
49
+ end
50
+
51
+ def follow_redirect(original_uri, response)
52
+ location = response['location']
53
+
54
+ if location.nil? || location.empty?
55
+ raise ArgumentError, "Redirect response missing Location header"
56
+ end
57
+
58
+ if location.start_with?('http://', 'https://')
59
+ URI.parse(location)
60
+ else
61
+ URI.join(original_uri, location)
62
+ end
63
+ end
64
+
65
+ def fail_response(response)
66
+ raise ArgumentError, "Failed to fetch resource: #{response.code} #{response.message}"
67
+ end
68
+ end
69
+ end
70
+ end
@@ -268,19 +268,19 @@ module Axlsx
268
268
  RestrictionValidator.validate :vertical_alignment, VALID_VERTICAL_ALIGNMENT_VALUES, v
269
269
  end
270
270
 
271
- VALID_CONTENT_TYPE_VALUES = [TABLE_CT, WORKBOOK_CT, APP_CT, RELS_CT, STYLES_CT, XML_CT, WORKSHEET_CT, SHARED_STRINGS_CT, CORE_CT, CHART_CT, JPEG_CT, GIF_CT, PNG_CT, DRAWING_CT, COMMENT_CT, VML_DRAWING_CT, PIVOT_TABLE_CT, PIVOT_TABLE_CACHE_DEFINITION_CT].freeze
271
+ VALID_CONTENT_TYPE_VALUES = [TABLE_CT, WORKBOOK_CT, APP_CT, RELS_CT, STYLES_CT, THEME_CT, XML_CT, WORKSHEET_CT, SHARED_STRINGS_CT, CORE_CT, CHART_CT, JPEG_CT, GIF_CT, PNG_CT, DRAWING_CT, COMMENT_CT, VML_DRAWING_CT, PIVOT_TABLE_CT, PIVOT_TABLE_CACHE_DEFINITION_CT].freeze
272
272
 
273
273
  # Requires that the value is a valid content_type
274
- # TABLE_CT, WORKBOOK_CT, APP_CT, RELS_CT, STYLES_CT, XML_CT, WORKSHEET_CT, SHARED_STRINGS_CT, CORE_CT, CHART_CT, DRAWING_CT, COMMENT_CT are allowed
274
+ # TABLE_CT, WORKBOOK_CT, APP_CT, RELS_CT, STYLES_CT, THEME_CT, XML_CT, WORKSHEET_CT, SHARED_STRINGS_CT, CORE_CT, CHART_CT, DRAWING_CT, COMMENT_CT are allowed
275
275
  # @param [Any] v The value validated
276
276
  def self.validate_content_type(v)
277
277
  RestrictionValidator.validate :content_type, VALID_CONTENT_TYPE_VALUES, v
278
278
  end
279
279
 
280
- VALID_RELATIONSHIP_TYPE_VALUES = [XML_NS_R, TABLE_R, WORKBOOK_R, WORKSHEET_R, APP_R, RELS_R, CORE_R, STYLES_R, CHART_R, DRAWING_R, IMAGE_R, HYPERLINK_R, SHARED_STRINGS_R, COMMENT_R, VML_DRAWING_R, COMMENT_R_NULL, PIVOT_TABLE_R, PIVOT_TABLE_CACHE_DEFINITION_R].freeze
280
+ VALID_RELATIONSHIP_TYPE_VALUES = [XML_NS_R, TABLE_R, WORKBOOK_R, WORKSHEET_R, APP_R, RELS_R, CORE_R, STYLES_R, THEME_R, CHART_R, DRAWING_R, IMAGE_R, HYPERLINK_R, SHARED_STRINGS_R, COMMENT_R, VML_DRAWING_R, COMMENT_R_NULL, PIVOT_TABLE_R, PIVOT_TABLE_CACHE_DEFINITION_R].freeze
281
281
 
282
282
  # Requires that the value is a valid relationship_type
283
- # XML_NS_R, TABLE_R, WORKBOOK_R, WORKSHEET_R, APP_R, RELS_R, CORE_R, STYLES_R, CHART_R, DRAWING_R, IMAGE_R, HYPERLINK_R, SHARED_STRINGS_R are allowed
283
+ # XML_NS_R, TABLE_R, WORKBOOK_R, WORKSHEET_R, APP_R, RELS_R, CORE_R, STYLES_R, THEME_R, CHART_R, DRAWING_R, IMAGE_R, HYPERLINK_R, SHARED_STRINGS_R are allowed
284
284
  # @param [Any] v The value validated
285
285
  def self.validate_relationship_type(v)
286
286
  RestrictionValidator.validate :relationship_type, VALID_RELATIONSHIP_TYPE_VALUES, v
data/lib/axlsx/version.rb CHANGED
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Axlsx
4
4
  # The current version
5
- VERSION = "4.3.0"
5
+ VERSION = "4.4.0"
6
6
  end
@@ -184,6 +184,12 @@ module Axlsx
184
184
  @styles
185
185
  end
186
186
 
187
+ # The theme associated with this workbook
188
+ # @return [Theme]
189
+ def theme
190
+ @theme ||= Theme.new
191
+ end
192
+
187
193
  # An array that holds all cells with styles
188
194
  # @return Set
189
195
  def styled_cells
@@ -373,6 +379,7 @@ module Axlsx
373
379
  r << Relationship.new(pivot_table.cache_definition, PIVOT_TABLE_CACHE_DEFINITION_R, format(PIVOT_TABLE_CACHE_DEFINITION_PN, index + 1))
374
380
  end
375
381
  r << Relationship.new(self, STYLES_R, STYLES_PN)
382
+ r << Relationship.new(self, THEME_R, THEME_PN)
376
383
  if use_shared_strings
377
384
  r << Relationship.new(self, SHARED_STRINGS_R, SHARED_STRINGS_PN)
378
385
  end
@@ -38,7 +38,7 @@ module Axlsx
38
38
  # @option [Boolean] show_horizontal_scroll Specifies a boolean value that indicates whether to display the horizontal scroll bar in the user interface.
39
39
  # @option [Boolean] show_vertical_scroll Specifies a boolean value that indicates whether to display the vertical scroll bar.
40
40
  # @option [Boolean] show_sheet_tabs Specifies a boolean value that indicates whether to display the sheet tabs in the user interface.
41
- # @option [Integer] tab_ratio Specifies ratio between the workbook tabs bar and the horizontal scroll bar.
41
+ # @option [Integer] tab_ratio Specifies the ratio between the workbook tabs bar and the horizontal scroll bar (from 0 to 1000, higher values mean more space for tabs). May only be supported on some clients.
42
42
  # @option [Integer] first_sheet Specifies the index to the first sheet in this book view.
43
43
  # @option [Integer] active_tab Specifies an unsignedInt that contains the index to the active sheet in this book view.
44
44
  # @option [Integer] x_window Specifies the X coordinate for the upper left corner of the workbook window. The unit of measurement for this value is twips.
data/lib/axlsx.rb CHANGED
@@ -13,6 +13,7 @@ require 'cgi'
13
13
  require 'set'
14
14
  require 'time'
15
15
  require 'uri'
16
+ require 'net/http'
16
17
 
17
18
  require_relative 'axlsx/util/simple_typed_list'
18
19
  require_relative 'axlsx/util/constants'
@@ -20,6 +21,7 @@ require_relative 'axlsx/util/validators'
20
21
  require_relative 'axlsx/util/accessors'
21
22
  require_relative 'axlsx/util/serialized_attributes'
22
23
  require_relative 'axlsx/util/options_parser'
24
+ require_relative 'axlsx/util/uri_utils'
23
25
  require_relative 'axlsx/util/mime_type_utils'
24
26
  require_relative 'axlsx/util/buffered_zip_output_stream'
25
27
  require_relative 'axlsx/util/zip_command'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: caxlsx
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.3.0
4
+ version: 4.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Randy Morgan
@@ -176,6 +176,7 @@ files:
176
176
  - lib/axlsx/stylesheet/table_style.rb
177
177
  - lib/axlsx/stylesheet/table_style_element.rb
178
178
  - lib/axlsx/stylesheet/table_styles.rb
179
+ - lib/axlsx/stylesheet/theme.rb
179
180
  - lib/axlsx/stylesheet/xf.rb
180
181
  - lib/axlsx/util/accessors.rb
181
182
  - lib/axlsx/util/buffered_zip_output_stream.rb
@@ -185,6 +186,7 @@ files:
185
186
  - lib/axlsx/util/serialized_attributes.rb
186
187
  - lib/axlsx/util/simple_typed_list.rb
187
188
  - lib/axlsx/util/storage.rb
189
+ - lib/axlsx/util/uri_utils.rb
188
190
  - lib/axlsx/util/validators.rb
189
191
  - lib/axlsx/util/zip_command.rb
190
192
  - lib/axlsx/version.rb