Class: Xlsxrb::Ooxml::ZipWriter

Inherits:
Object
  • Object
show all
Defined in:
lib/xlsxrb/ooxml/zip_writer.rb,
sig/generated/xlsxrb/ooxml/zip_writer.rbs

Overview

Writes ZIP archives using only stdlib (zlib). Supports streaming: entries are written sequentially, central directory at close.

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(io) ⇒ ZipWriter

Returns a new instance of ZipWriter.

Parameters:

  • io (Object)


27
28
29
30
31
32
33
# File 'lib/xlsxrb/ooxml/zip_writer.rb', line 27

def initialize(io)
  @io = io
  @entries = []
  @closed = false
  @bytes_written = 0
  @seekable = determine_seekable
end

Class Method Details

.open(target, &block) ⇒ void

This method returns an undefined value.

Parameters:

  • target (Object)


12
13
14
15
16
17
18
19
20
21
22
23
24
25
# File 'lib/xlsxrb/ooxml/zip_writer.rb', line 12

def self.open(target, &block)
  io = target.is_a?(String) ? File.open(target, "wb") : target
  writer = new(io)
  if block
    begin
      yield writer
    ensure
      writer.close
      io.close if target.is_a?(String)
    end
  else
    writer
  end
end

Instance Method Details

#add_binary_entry(path, content) ⇒ Object

Add a file entry with binary content (no encoding conversion).

Parameters:

  • path (Object)
  • content (Object)

Returns:

  • (Object)


44
45
46
47
48
49
# File 'lib/xlsxrb/ooxml/zip_writer.rb', line 44

def add_binary_entry(path, content)
  raise "ZipWriter is closed" if @closed

  content_bytes = content.b
  write_raw_entry(path, content_bytes)
end

#add_entry(path, content) ⇒ Object

Add a file entry with string content.

Parameters:

  • path (Object)
  • content (Object)

Returns:

  • (Object)


36
37
38
39
40
41
# File 'lib/xlsxrb/ooxml/zip_writer.rb', line 36

def add_entry(path, content)
  raise "ZipWriter is closed" if @closed

  content_bytes = content.encode("UTF-8").b
  write_raw_entry(path, content_bytes)
end

#closeObject

Returns:

  • (Object)


123
124
125
126
127
128
129
130
131
132
# File 'lib/xlsxrb/ooxml/zip_writer.rb', line 123

def close
  return if @closed

  finish_entry if @current_entry

  @closed = true
  cd_offset = @bytes_written
  cd_size = write_central_directory
  write_end_of_central_directory(cd_offset, cd_size)
end

#deflate(content) ⇒ Object

Parameters:

  • content (Object)

Returns:

  • (Object)


173
174
175
176
177
178
179
180
# File 'lib/xlsxrb/ooxml/zip_writer.rb', line 173

def deflate(content)
  deflater = Zlib::Deflate.new(Zlib::DEFAULT_COMPRESSION, -Zlib::MAX_WBITS)
  begin
    deflater.deflate(content, Zlib::FINISH)
  ensure
    deflater.close
  end
end

#determine_seekableObject

Returns:

  • (Object)


136
137
138
139
140
141
142
143
144
145
146
# File 'lib/xlsxrb/ooxml/zip_writer.rb', line 136

def determine_seekable
  return true if @io.is_a?(StringIO)
  return false unless @io.respond_to?(:seek) && @io.respond_to?(:tell)

  begin
    @io.tell
    true
  rescue SystemCallError, IOError
    false
  end
end

#finish_entryObject

Returns:

  • (Object)


87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
# File 'lib/xlsxrb/ooxml/zip_writer.rb', line 87

def finish_entry
  raise "No entry started" unless @current_entry

  entry = @current_entry
  @current_entry = nil

  # Flush remaining deflate data
  remaining = entry[:deflater].finish
  entry[:compressed_size] += remaining.bytesize
  write_bytes(remaining)
  entry[:deflater].close

  final_crc = entry[:crc] & 0xFFFFFFFF

  if entry[:seekable]
    # Patch the local header if seekable
    current_pos = @io.is_a?(StringIO) ? @io.pos : @io.tell
    @io.seek(entry[:offset])
    write_local_header(entry[:path], final_crc, entry[:compressed_size], entry[:uncompressed_size], gp_flag: 0, count_bytes: false)
    @io.seek(current_pos)
  else
    # Write Data Descriptor (signature 0x08074b50 + crc32 + comp_size + uncomp_size)
    descriptor = [0x08074B50, final_crc, entry[:compressed_size], entry[:uncompressed_size]].pack("VVVV")
    write_bytes(descriptor)
  end

  @entries << {
    path: entry[:path],
    crc32: final_crc,
    compressed_size: entry[:compressed_size],
    uncompressed_size: entry[:uncompressed_size],
    offset: entry[:offset],
    gp_flag: entry[:gp_flag]
  }
end

#start_entry(path) ⇒ Object

Write a string directly into the current ZIP entry stream. Use start_entry / write_data / finish_entry for true streaming.

Parameters:

  • path (Object)

Returns:

  • (Object)


53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# File 'lib/xlsxrb/ooxml/zip_writer.rb', line 53

def start_entry(path)
  raise "ZipWriter is closed" if @closed

  entry_offset = @bytes_written
  gp_flag = @seekable ? 0 : 0x0008

  @current_entry = {
    path: path,
    offset: entry_offset,
    deflater: Zlib::Deflate.new(Zlib::DEFAULT_COMPRESSION, -Zlib::MAX_WBITS),
    crc: Zlib.crc32,
    uncompressed_size: 0,
    compressed_size: 0,
    gp_flag: gp_flag,
    seekable: @seekable
  }

  # Write local header (if unseekable, sizes/crc are 0 and bit 3 is set)
  write_local_header(path, 0, 0, 0, gp_flag: gp_flag)
end

#write_bytes(data) ⇒ Object

Parameters:

  • data (Object)

Returns:

  • (Object)


148
149
150
151
152
153
# File 'lib/xlsxrb/ooxml/zip_writer.rb', line 148

def write_bytes(data)
  return if data.nil? || data.empty?

  @io.write(data)
  @bytes_written += data.bytesize
end

#write_central_directoryObject

Returns:

  • (Object)


207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
# File 'lib/xlsxrb/ooxml/zip_writer.rb', line 207

def write_central_directory
  size = 0
  @entries.each do |entry|
    name_bytes = entry[:path].encode("UTF-8").b
    header = [
      0x02014B50,        # central directory file header signature
      20,                # version made by
      20,                # version needed
      entry[:gp_flag] || 0, # general purpose bit flag
      8,                 # compression method
      0,                 # last mod file time
      33,                # last mod file date
      entry[:crc32],
      entry[:compressed_size],
      entry[:uncompressed_size],
      name_bytes.bytesize,
      0,                 # extra field length
      0,                 # file comment length
      0,                 # disk number start
      0,                 # internal file attributes
      0,                 # external file attributes
      entry[:offset]     # relative offset of local header
    ].pack("VvvvvvvVVVvvvvvVV")

    write_bytes(header)
    write_bytes(name_bytes)
    size += header.bytesize + name_bytes.bytesize
  end
  size
end

#write_data(str) ⇒ Object

Parameters:

  • str (Object)

Returns:

  • (Object)


74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/xlsxrb/ooxml/zip_writer.rb', line 74

def write_data(str)
  raise "No entry started" unless @current_entry

  bytes = str.b
  return if bytes.empty?

  @current_entry[:crc] = Zlib.crc32(bytes, @current_entry[:crc])
  @current_entry[:uncompressed_size] += bytes.bytesize
  compressed = @current_entry[:deflater].deflate(bytes, Zlib::SYNC_FLUSH)
  @current_entry[:compressed_size] += compressed.bytesize
  write_bytes(compressed)
end

#write_end_of_central_directory(cd_offset, cd_size) ⇒ Object

Parameters:

  • cd_offset (Object)
  • cd_size (Object)

Returns:

  • (Object)


238
239
240
241
242
243
244
245
246
247
248
249
250
# File 'lib/xlsxrb/ooxml/zip_writer.rb', line 238

def write_end_of_central_directory(cd_offset, cd_size)
  eocd = [
    0x06054B50,          # end of central dir signature
    0,                   # disk number
    0,                   # disk with central directory
    @entries.size,       # entries on this disk
    @entries.size,       # total entries
    cd_size,             # size of central directory
    cd_offset,           # offset of central directory
    0                    # comment length
  ].pack("VvvvvVVv")
  write_bytes(eocd)
end

#write_local_header(path, crc, compressed_size, uncompressed_size, gp_flag: 0, count_bytes: true) ⇒ Object

Parameters:

  • path (Object)
  • crc (Object)
  • compressed_size (Object)
  • uncompressed_size (Object)
  • gp_flag: (Object) (defaults to: 0)
  • count_bytes: (Object) (defaults to: true)

Returns:

  • (Object)


182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
# File 'lib/xlsxrb/ooxml/zip_writer.rb', line 182

def write_local_header(path, crc, compressed_size, uncompressed_size, gp_flag: 0, count_bytes: true)
  name_bytes = path.encode("UTF-8").b
  header = [
    0x04034B50,        # local file header signature
    20,                # version needed (2.0)
    gp_flag,           # general purpose bit flag (0x0008 if data descriptor follows)
    8,                 # compression method (deflate)
    0,                 # last mod file time
    33,                # last mod file date
    crc,
    compressed_size,
    uncompressed_size,
    name_bytes.bytesize,
    0                  # extra field length
  ].pack("VvvvvvVVVvv")

  if count_bytes
    write_bytes(header)
    write_bytes(name_bytes)
  else
    @io.write(header)
    @io.write(name_bytes)
  end
end

#write_raw_entry(path, content_bytes) ⇒ Object

Parameters:

  • path (Object)
  • content_bytes (Object)

Returns:

  • (Object)


155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
# File 'lib/xlsxrb/ooxml/zip_writer.rb', line 155

def write_raw_entry(path, content_bytes)
  crc = Zlib.crc32(content_bytes) & 0xFFFFFFFF
  compressed = deflate(content_bytes)
  offset = @bytes_written

  write_local_header(path, crc, compressed.bytesize, content_bytes.bytesize, gp_flag: 0)
  write_bytes(compressed)

  @entries << {
    path: path,
    crc32: crc,
    compressed_size: compressed.bytesize,
    uncompressed_size: content_bytes.bytesize,
    offset: offset,
    gp_flag: 0
  }
end