standard-procedure-consolidate 0.3.9 → 0.4.1

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: cb70b369911ba5aa2dc64c21a460b690b722f947e457f3d3efcacf365577046a
4
- data.tar.gz: 010f296150c641bc92be1b7255ae424f43393205baa11384013003fb55f056ba
3
+ metadata.gz: 0ffce925315399bd4432936d929e995114acc2dea081f3835eb437fe2787f612
4
+ data.tar.gz: b64ae5181ae92d5e66be79e5861787c3355fe08870dbb4ee3ab8355ef4ce149d
5
5
  SHA512:
6
- metadata.gz: 824f18d218c9489467cfb62ec7567b5400b7f5e08f477864ed1553dbe0a4a278747002f02ee420c1877c2cb659c5b5f456d1716d4a9ffe4b7a8e825641d9bd26
7
- data.tar.gz: f6dabb52a96bea36e61b90fcac99d6aedd2a58090d3ddfe3a84d76b19d1672e969cd6728221b51a9b12e83cf83801481626405ebd89c04632de13dba5c87889f
6
+ metadata.gz: e56547c32223cf66f807c804cdfff2897f63887729dcf1ff80d51c35a0615115f47b16347b582d58a6ae128c3ecad91a67ca128e0d7c89b6e55a3e88b2d5a6c9
7
+ data.tar.gz: 4b351ad3a367dd4382abc18e18fbc978095fec92aa8b258a90feb82f2e0a65ffbcca26a8177383d75fab07d711a170b10b541e48c195c4b8b7db0eea2cfd1b26
data/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ # [0.4.1] - 2024-12-18
2
+
3
+ Replace image merge fields with blanks if the image data is not provided
4
+
5
+ # [0.4.0] - 2024-12-16
6
+
7
+ Image embedding works
8
+
1
9
  # [0.3.9] - 2024-12-4
2
10
 
3
11
  Image embedding - not fully tested but it seems to work in a few test cases
@@ -0,0 +1 @@
1
+ 58d1597374e775340c60e3e49b9d65437909b482f7be3040fb9fdca5d8d4b4d3020508a9ee01e3521b2c552e1b853d0475335d1ca1c092b4172c1e9ac86df640
@@ -0,0 +1 @@
1
+ dfd2ec36154a632f86dd8f99a2b85c59ed6048dbb6b8100f84645ea1cf49e3e77ad73b3367ef89d46e055c860f1b1e82c5b0d0d37c969a19c2644a3e42ebbe21
@@ -13,12 +13,20 @@ module Consolidate
13
13
  def storage_path = "word/#{media_path}"
14
14
 
15
15
  # Convert width from pixels to EMU
16
- def width = super * EMU_PER_PIXEL
16
+ def width = super * emu_per_width_pixel
17
17
 
18
18
  # Convert height from pixels to EMU
19
- def height = super * EMU_PER_PIXEL
19
+ def height = super * emu_per_height_pixel
20
20
 
21
- EMU_PER_PIXEL = (914400 / 72)
21
+ # Get the width of this image in EMU up to a maximum page width (also in EMU)
22
+ def clamped_width(maximum = 7_772_400) = [width, maximum].min
23
+
24
+ # Get the height of this image in EMU adjusted for a maximum page width (also in EMU)
25
+ def clamped_height(maximum = 7_772_400) = (height * clamped_width(maximum).to_f / width.to_f).to_i
26
+
27
+ def emu_per_width_pixel = 914_400 / dpi[:x]
28
+
29
+ def emu_per_height_pixel = 914_400 / dpi[:y]
22
30
  end
23
31
  end
24
32
  end
@@ -5,74 +5,67 @@ require "nokogiri"
5
5
 
6
6
  module Consolidate
7
7
  module Docx
8
- class ImageReferenceNodeBuilder < Data.define(:field_name, :image, :node_id, :document)
8
+ class ImageReferenceNodeBuilder < Data.define(:field_name, :image, :node_id, :image_number, :document)
9
9
  def call
10
- Nokogiri::XML::Node.new("w:r", document).tap do |run_node|
11
- run_node << Nokogiri::XML::Node.new("w:drawing", document).tap do |drawing|
12
- drawing << Nokogiri::XML::Node.new("wp:inline", document).tap do |inline|
13
- inline << Nokogiri::XML::Node.new("wp:extend", document).tap do |extent|
14
- extent["cx"] = image.width.to_s
15
- extent["cy"] = image.height.to_s
16
- end
17
- inline << Nokogiri::XML::Node.new("wp:effectExtent", document).tap do |effect_extent|
18
- effect_extent["l"] = "0"
19
- effect_extent["t"] = "0"
20
- effect_extent["r"] = "0"
21
- effect_extent["b"] = "0"
22
- end
23
- inline << Nokogiri::XML::Node.new("wp:docPr", document).tap do |doc_pr|
24
- doc_pr["id"] = node_id
25
- doc_pr["name"] = field_name
26
- end
27
- inline << Nokogiri::XML::Node.new("wp:cNvGraphicFramePr", document) do |c_nv_graphic_frame_pr|
28
- c_nv_graphic_frame_pr << Nokogiri::XML::Node.new("a:graphicFrameLocks", document).tap do |graphic_frame_locks|
29
- graphic_frame_locks["xmlns:a"] = "http://schemas.openxmlformats.org/drawingml/2006/main"
30
- graphic_frame_locks["noChangeAspect"] = "1"
31
- end
10
+ Nokogiri::XML::Node.new("w:drawing", document).tap do |drawing|
11
+ drawing["xmlns:a"] = "http://schemas.openxmlformats.org/drawingml/2006/main"
12
+ drawing << Nokogiri::XML::Node.new("wp:inline", document).tap do |inline|
13
+ inline["distT"] = "0"
14
+ inline["distB"] = "0"
15
+ inline["distL"] = "0"
16
+ inline["distR"] = "0"
17
+ inline << Nokogiri::XML::Node.new("wp:extent", document).tap do |extent|
18
+ extent["cx"] = image.clamped_width(max_width_from(document))
19
+ extent["cy"] = image.clamped_height(max_width_from(document))
20
+ end
21
+ inline << Nokogiri::XML::Node.new("wp:effectExtent", document).tap do |effect_extent|
22
+ effect_extent["l"] = "0"
23
+ effect_extent["t"] = "0"
24
+ effect_extent["r"] = "0"
25
+ effect_extent["b"] = "0"
26
+ end
27
+ inline << Nokogiri::XML::Node.new("wp:cNvGraphicFramePr", document).tap do |c_nv_graphic_frame_pr|
28
+ c_nv_graphic_frame_pr << Nokogiri::XML::Node.new("a:graphicFrameLocks", document).tap do |graphic_frame_locks|
29
+ graphic_frame_locks["noChangeAspect"] = true
32
30
  end
33
- inline << Nokogiri::XML::Node.new("a:graphic", document).tap do |graphic|
34
- graphic["xmlns:a"] = "http://schemas.openxmlformats.org/drawingml/2006/main"
35
- graphic << Nokogiri::XML::Node.new("a:graphicData", document).tap do |graphic_data|
36
- graphic_data["uri"] = "http://schemas.openxmlformats.org/drawingml/2006/picture"
37
- graphic_data << Nokogiri::XML::Node.new("pic:pic", document).tap do |pic|
38
- pic["xmlns:pic"] = "http://schemas.openxmlformats.org/drawingml/2006/picture"
39
- pic << Nokogiri::XML::Node.new("pic:nvPicPr", document).tap do |nv_pic_pr|
40
- nv_pic_pr << Nokogiri::XML::Node.new("pic:cNvPr", document).tap do |c_nv_pr|
41
- c_nv_pr["id"] = node_id
42
- c_nv_pr["name"] = field_name
43
- end
44
- nv_pic_pr << Nokogiri::XML::Node.new("pic:cNvPicPr", document).tap do |c_nv_pic_pr|
45
- c_nv_pic_pr << Nokogiri::XML::Node.new("a:picLocks", document).tap do |pic_locks|
46
- pic_locks["noChangeAspect"] = "1"
47
- end
48
- end
31
+ end
32
+ inline << Nokogiri::XML::Node.new("a:graphic", document).tap do |graphic|
33
+ graphic["xmlns:a"] = "http://schemas.openxmlformats.org/drawingml/2006/main"
34
+ graphic << Nokogiri::XML::Node.new("a:graphicData", document).tap do |graphic_data|
35
+ graphic_data["uri"] = "http://schemas.openxmlformats.org/drawingml/2006/picture"
36
+ graphic_data << Nokogiri::XML::Node.new("pic:pic", document).tap do |pic|
37
+ pic["xmlns:pic"] = "http://schemas.openxmlformats.org/drawingml/2006/picture"
38
+ pic << Nokogiri::XML::Node.new("pic:nvPicPr", document).tap do |nv_pic_pr|
39
+ nv_pic_pr << Nokogiri::XML::Node.new("pic:cNvPr", document).tap do |c_nv_pr|
40
+ c_nv_pr["id"] = image_number
41
+ c_nv_pr["name"] = image.name
42
+ c_nv_pr["descr"] = image.name
43
+ c_nv_pr["hidden"] = false
44
+ c_nv_pr << Nokogiri::XML::Node.new("pic:cNvPicPr", document)
49
45
  end
50
- pic << Nokogiri::XML::Node.new("pic:blipFill", document).tap do |blip_fill|
51
- blip_fill << Nokogiri::XML::Node.new("a:blip", document).tap do |blip|
52
- blip["r:embed"] = node_id
53
- blip["xmlns:r"] = "http://schemas.openxmlformats.org/officeDocument/2006/relationships"
54
- blip << Nokogiri::XML::Node.new("a:extLst", document)
55
- end
56
- blip_fill << Nokogiri::XML::Node.new("a:stretch", document).tap do |stretch|
57
- stretch << Nokogiri::XML::Node.new("a:fillRect", document)
58
- end
46
+ end
47
+ pic << Nokogiri::XML::Node.new("pic:blipFill", document).tap do |blip_fill|
48
+ blip_fill << Nokogiri::XML::Node.new("a:blip", document).tap do |blip|
49
+ blip["r:embed"] = node_id
59
50
  end
60
- pic << Nokogiri::XML::Node.new("pic:spPr", document).tap do |sp_pr|
61
- sp_pr << Nokogiri::XML::Node.new("a:xfrm", document).tap do |xfrm|
62
- xfrm << Nokogiri::XML::Node.new("a:off", document).tap do |off|
63
- off["x"] = "0"
64
- off["y"] = "0"
65
- end
66
- xfrm << Nokogiri::XML::Node.new("a:ext", document).tap do |ext|
67
- ext["cx"] = image.width.to_s
68
- ext["cy"] = image.height.to_s
69
- end
51
+ blip_fill << Nokogiri::XML::Node.new("a:stretch", document).tap do |stretch|
52
+ stretch << Nokogiri::XML::Node.new("a:fillRect", document)
53
+ end
54
+ end
55
+ pic << Nokogiri::XML::Node.new("pic:spPr", document).tap do |sp_pr|
56
+ sp_pr << Nokogiri::XML::Node.new("a:xfrm", document).tap do |xfrm|
57
+ xfrm << Nokogiri::XML::Node.new("a:off", document).tap do |off|
58
+ off["x"] = "0"
59
+ off["y"] = "0"
70
60
  end
71
- sp_pr << Nokogiri::XML::Node.new("a:prstGeom", document).tap do |prst_geom|
72
- prst_geom["prst"] = "rect"
73
- prst_geom << Nokogiri::XML::Node.new("a:avLst", document)
61
+ xfrm << Nokogiri::XML::Node.new("a:ext", document).tap do |ext|
62
+ ext["cx"] = image.clamped_width(max_width_from(document))
63
+ ext["cy"] = image.clamped_height(max_width_from(document))
74
64
  end
75
- sp_pr << Nokogiri::XML::Node.new("a:effectLst", document)
65
+ end
66
+ sp_pr << Nokogiri::XML::Node.new("a:prstGeom", document).tap do |prst_geom|
67
+ prst_geom["prst"] = "rect"
68
+ prst_geom << Nokogiri::XML::Node.new("a:avLst", document)
76
69
  end
77
70
  end
78
71
  end
@@ -81,6 +74,15 @@ module Consolidate
81
74
  end
82
75
  end
83
76
  end
77
+
78
+ DEFAULT_PAGE_WIDTH = 12_240
79
+ TWENTIETHS_OF_A_POINT_TO_EMU = 635
80
+ DEFAULT_PAGE_WIDTH_IN_EMU = DEFAULT_PAGE_WIDTH * TWENTIETHS_OF_A_POINT_TO_EMU
81
+
82
+ private def max_width_from document
83
+ page_width = (document.at_xpath("//w:sectPr/w:pgSz/@w:w")&.value || DEFAULT_PAGE_WIDTH).to_i
84
+ page_width * TWENTIETHS_OF_A_POINT_TO_EMU
85
+ end
84
86
  end
85
87
  end
86
88
  end
@@ -20,13 +20,16 @@ module Consolidate
20
20
  @zip = Zip::File.open(path)
21
21
  @documents = load_documents
22
22
  @relations = load_relations
23
+ @contents_xml = load_and_update_contents_xml
23
24
  @output = {}
24
25
  @images = {}
26
+ @mapping = {}
25
27
  end
26
28
 
27
29
  # Helper method to display the contents of the document and the merge fields from the CLI
28
30
  def examine
29
31
  puts "Documents: #{document_names.join(", ")}"
32
+ puts "Content documents: #{content_document_names.join(", ")}"
30
33
  puts "Merge fields: #{text_field_names.join(", ")}"
31
34
  puts "Image fields: #{image_field_names.join(", ")}"
32
35
  end
@@ -38,29 +41,48 @@ module Consolidate
38
41
  def image_field_names = @image_field_names ||= tag_nodes.collect { |tag_node| image_field_names_from tag_node }.flatten.compact.uniq
39
42
 
40
43
  # List the documents stored within this docx
41
- def document_names = @zip.entries.collect { |entry| entry.name }
44
+ def document_names = @zip.entries.map(&:name)
45
+
46
+ # List the content within this docx
47
+ def content_document_names = @documents.keys
48
+
49
+ # List the field names that are present in the merge data
50
+ def merge_field_names = @mapping.keys
42
51
 
43
52
  # Set the merge data and erform the substitution - creating copies of any documents that contain merge tags and replacing the tags with the supplied data
44
53
  def data mapping = {}
45
- mapping = mapping.transform_keys(&:to_s)
46
- puts mapping.keys.select { |field_name| text_field_names.include?(field_name) }.map { |field_name| "#{field_name} => #{mapping[field_name]}" }.join("\n") if verbose
54
+ @mapping = mapping.transform_keys(&:to_s)
55
+ if verbose
56
+ puts "...mapping data"
57
+ puts @mapping.keys.select { |field_name| text_field_names.include?(field_name) }.map { |field_name| "... #{field_name} => #{@mapping[field_name]}" }.join("\n")
58
+ end
47
59
 
48
- @images = load_images_and_link_relations_from mapping
60
+ @images = load_images_and_link_relations
49
61
 
50
62
  @documents.each do |name, document|
51
- @output[name] = substitute(document.dup, mapping: mapping, document_name: name).serialize save_with: 0
63
+ @output[name] = substitute(document.dup, document_name: name).serialize save_with: 0
52
64
  end
53
65
  end
54
66
 
55
67
  def write_to path
56
68
  puts "...writing to #{path}" if verbose
57
69
  Zip::File.open(path, Zip::File::CREATE) do |out|
70
+ @output[contents_xml] = @contents_xml.serialize save_with: 0
71
+
58
72
  @images.each do |field_name, image|
59
- puts "... writing #{field_name} to #{image.storage_path}" if verbose
73
+ next if image.nil?
74
+ puts "... writing image #{field_name} to #{image.storage_path}" if verbose
60
75
  out.get_output_stream(image.storage_path) { |o| o.write image.contents }
61
76
  end
62
77
 
63
- @zip.each do |entry|
78
+ @relations.each do |relation_name, relations|
79
+ puts "... writing relations #{relation_name}" if verbose
80
+ out.get_output_stream(relation_name) { |o| o.write relations }
81
+ end
82
+
83
+ @zip.reject do |entry|
84
+ @relations.key? entry.name
85
+ end.each do |entry|
64
86
  puts "... writing updated document to #{entry.name}" if verbose
65
87
  out.get_output_stream(entry.name) { |o| o.write(@output[entry.name] || @relations[entry.name] || @zip.read(entry.name)) }
66
88
  end
@@ -71,25 +93,7 @@ module Consolidate
71
93
 
72
94
  attr_reader :verbose
73
95
 
74
- def load_documents
75
- @zip.entries.each_with_object({}) do |entry, results|
76
- next unless entry.name.match?(/word\/(document|header|footer|footnotes|endnotes).?\.xml/)
77
- puts "...reading document #{entry.name}" if verbose
78
- contents = @zip.get_input_stream entry
79
- results[entry.name] = Nokogiri::XML(contents) { |x| x.noent }
80
- end
81
- end
82
-
83
- def load_relations
84
- @zip.entries.each_with_object({}) do |entry, results|
85
- next unless entry.name.match?(/word\/_rels\/.*.rels/)
86
- puts "...reading relation #{entry.name}" if verbose
87
- contents = @zip.get_input_stream entry
88
- results[entry.name] = Nokogiri::XML(contents) { |x| x.noent }
89
- end
90
- ensure
91
- @zip.close
92
- end
96
+ def contents_xml = "[Content_Types].xml"
93
97
 
94
98
  # Regex to find merge fields that contain text
95
99
  def text_tag = /\{\{\s*(?!.*_image\b)(\S+)\s*\}\}/i
@@ -113,30 +117,61 @@ module Consolidate
113
117
  # Extract the image field name(s) from the paragraph
114
118
  def image_field_names_from(tag_node) = (matches = tag_node.content.scan(image_tag)).empty? ? nil : matches.flatten.map(&:strip)
115
119
 
120
+ # Unique number for each image field
121
+ def relation_number_for(field_name) = @mapping.keys.index(field_name) + 1000
122
+
116
123
  # Identifier to use when linking a merge field to the actual image file contents
117
124
  def relation_id_for(field_name) = "rId#{field_name}"
118
125
 
126
+ # Empty elations document for documents that do not already have one
127
+ def default_relations_document = %(<?xml version="1.0" encoding="UTF-8"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"></Relationships>)
128
+
129
+ def load_documents
130
+ @zip.entries.each_with_object({}) do |entry, results|
131
+ next unless entry.name.match?(/word\/(document|header|footer|footnotes|endnotes).?\.xml/)
132
+ puts "...reading document #{entry.name}" if verbose
133
+ contents = @zip.get_input_stream entry
134
+ results[entry.name] = Nokogiri::XML(contents) { |x| x.noent }
135
+ end
136
+ end
137
+
138
+ def load_relations
139
+ @zip.entries.each_with_object({}) do |entry, results|
140
+ next unless entry.name.match?(/word\/(document|header|footer|footnotes|endnotes).?\.xml/)
141
+ relation_document = entry.name.gsub("word/", "word/_rels/").gsub(".xml", ".xml.rels")
142
+ puts "...reading or building relations for #{relation_document}" if verbose
143
+ contents = @zip.find_entry(relation_document) ? @zip.get_input_stream(relation_document) : default_relations_document
144
+ results[relation_document] = Nokogiri::XML(contents) { |x| x.noent }
145
+ end
146
+ ensure
147
+ @zip.close
148
+ end
149
+
119
150
  # Create relation links for each image field and store the image data
120
- def load_images_and_link_relations_from mapping
121
- load_images_from(mapping).tap do |images|
151
+ def load_images_and_link_relations
152
+ load_images.tap do |images|
122
153
  link_relations_to images
123
154
  end
124
155
  end
125
156
 
126
157
  # Build a mapping of image paths to the image data so that the image data can be stored in the output docx
127
- def load_images_from mapping = {}
158
+ def load_images
128
159
  image_field_names.each_with_object({}) do |field_name, result|
129
- result[field_name] = Consolidate::Docx::Image.new(mapping[field_name])
160
+ result[field_name] = @mapping[field_name].nil? ? nil : Consolidate::Docx::Image.new(@mapping[field_name])
161
+ puts "... #{field_name} => #{result[field_name]&.media_path}" if verbose
130
162
  end
131
163
  end
132
164
 
133
165
  # Update all relation documents to include a relationship for each image field and its stored image path
134
166
  def link_relations_to images
135
167
  @relations.each do |name, xml|
168
+ puts "... linking images in #{name}" if verbose
136
169
  images.each do |field_name, image|
170
+ # Has an actual image file been supplied?
171
+ next if image.nil?
137
172
  # Is this image already referenced in this relationship document?
138
- next unless xml.at_xpath("//Relationship[@Target='#{image.media_path}']").nil?
139
- puts "...linking #{field_name} to #{image.media_path}" if verbose
173
+ next unless xml.at_xpath("//Relationship[@Target=\"#{image.media_path}\"]").nil?
174
+ puts "... #{relation_id_for(field_name)} => #{image.media_path}" if verbose
140
175
  xml.root << Nokogiri::XML::Node.new("Relationship", xml).tap do |relation|
141
176
  relation["Id"] = relation_id_for(field_name)
142
177
  relation["Type"] = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image"
@@ -146,32 +181,38 @@ module Consolidate
146
181
  end
147
182
  end
148
183
 
184
+ def load_and_update_contents_xml
185
+ puts "...reading and updating #{contents_xml}" if verbose
186
+ content = @zip.get_input_stream(contents_xml)
187
+ Nokogiri::XML(content) { |x| x.noent }.tap do |document|
188
+ add_content_relations_to document
189
+ end
190
+ end
191
+
149
192
  # Go through the given document, replacing any merge fields with the values provided
150
193
  # and storing the results in a new document
151
- def substitute document, document_name:, mapping: {}
194
+ def substitute document, document_name:
195
+ puts "...substituting fields in #{document_name}" if verbose && tag_nodes_for(document).any?
196
+ substitute_text document, document_name: document_name
197
+ substitute_images document, document_name: document_name
198
+ end
199
+
200
+ def substitute_text document, document_name:
152
201
  tag_nodes_for(document).each do |tag_node|
153
- text_field_names = text_field_names_from(tag_node) || []
154
- image_field_names = image_field_names_from(tag_node) || []
202
+ field_names = text_field_names_from(tag_node) || []
155
203
 
156
204
  # Extract the properties (formatting) nodes if they exist
157
205
  paragraph_properties = tag_node.search ".//w:pPr"
158
206
  run_properties = tag_node.at_xpath ".//w:rPr"
159
207
 
160
- # Get the current contents, then substitute any text fields, followed by any image fields
208
+ # Get the current contents, then substitute any text fields
161
209
  text = tag_node.content
162
210
 
163
- text_field_names.each do |field_name|
164
- field_value = mapping[field_name].to_s
165
- puts "...substituting #{field_name} with #{field_value} in #{document_name}" if verbose
211
+ field_names.each do |field_name|
212
+ field_value = @mapping[field_name].to_s
213
+ puts "... substituting '#{field_name}' with '#{field_value}'" if verbose
166
214
  text = text.gsub(tag_for(field_name), field_value)
167
215
  end
168
- image_nodes = image_field_names.collect do |field_name|
169
- image = @images[field_name]
170
- puts "...substituting #{field_name} in #{document_name}" if verbose
171
- # Remove the merge tag and create an image reference node to be added to this node
172
- text = text.gsub(tag_for(field_name), "")
173
- ImageReferenceNodeBuilder.new(field_name: field_name, image: image, node_id: relation_id_for(field_name), document: document).call
174
- end
175
216
 
176
217
  # Create a new text node with the substituted text
177
218
  text_node = Nokogiri::XML::Node.new("w:t", tag_node.document)
@@ -182,7 +223,46 @@ module Consolidate
182
223
  run_node << run_properties unless run_properties.nil?
183
224
  run_node << text_node
184
225
  # Add the paragraph properties and the run node to the tag node
185
- tag_node.children = Nokogiri::XML::NodeSet.new(document, paragraph_properties.to_a + [run_node] + image_nodes)
226
+ tag_node.children = Nokogiri::XML::NodeSet.new(document, paragraph_properties.to_a + [run_node])
227
+ rescue => ex
228
+ # Have to mangle the exception message otherwise it outputs the entire document
229
+ puts ex.message.to_s[0..255]
230
+ puts ex.backtrace.first
231
+ end
232
+ document
233
+ end
234
+
235
+ # Go through the given document, replacing any merge fields with the values provided
236
+ # and storing the results in a new document
237
+ def substitute_images document, document_name:
238
+ tag_nodes_for(document).each do |tag_node|
239
+ field_names = image_field_names_from(tag_node) || []
240
+ # Extract the properties (formatting) nodes if they exist
241
+ paragraph_properties = tag_node.search ".//w:pPr"
242
+ run_properties = tag_node.at_xpath ".//w:rPr"
243
+
244
+ pieces = tag_node.content.split(image_tag)
245
+ # Split the content into pieces - either text or an image merge field
246
+ # Then replace the text with text nodes or the image merge fields with drawing nodes
247
+ replacement_nodes = pieces.collect do |piece|
248
+ field_name = piece.strip
249
+ if field_names.include? field_name
250
+ image = @images[field_name]
251
+ # if no image was provided then insert blank text
252
+ # otherwise insert a w:drawing node that references the image contents
253
+ if image.nil?
254
+ puts "... substituting '#{field_name}' with blank as no image was provided" if verbose
255
+ Nokogiri::XML::Node.new("w:t", document) { |t| t.content = "" }
256
+ else
257
+ puts "... substituting '#{field_name}' with '<#{relation_id_for(field_name)}/>'" if verbose
258
+ ImageReferenceNodeBuilder.new(field_name: field_name, image: image, node_id: relation_id_for(field_name), image_number: relation_number_for(field_name), document: document).call
259
+ end
260
+ else
261
+ Nokogiri::XML::Node.new("w:t", document) { |t| t.content = piece }
262
+ end
263
+ end
264
+ run_nodes = (replacement_nodes.map { |node| Nokogiri::XML::Node.new("w:r", document) { |run_node| run_node.children = node } } + [run_properties]).compact
265
+ tag_node.children = Nokogiri::XML::NodeSet.new(document, paragraph_properties.to_a + run_nodes)
186
266
  rescue => ex
187
267
  # Have to mangle the exception message otherwise it outputs the entire document
188
268
  puts ex.message.to_s[0..255]
@@ -190,6 +270,26 @@ module Consolidate
190
270
  end
191
271
  document
192
272
  end
273
+
274
+ CONTENT_RELATIONS = {
275
+ jpeg: "image/jpg",
276
+ png: "image/png",
277
+ bmp: "image/bmp",
278
+ gif: "image/gif",
279
+ tif: "image/tif",
280
+ pdf: "application/pdf",
281
+ mov: "application/movie"
282
+ }.freeze
283
+
284
+ def add_content_relations_to document
285
+ CONTENT_RELATIONS.each do |file_type, content_type|
286
+ next unless document.at_xpath("//Default[@Extension=\"#{file_type}\"]").nil?
287
+ document.root << Nokogiri::XML::Node.new("Default", document).tap do |relation|
288
+ relation["Extension"] = file_type
289
+ relation["ContentType"] = content_type
290
+ end
291
+ end
292
+ end
193
293
  end
194
294
  end
195
295
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Consolidate
4
4
  class Image
5
- attr_reader :name, :width, :height
5
+ attr_reader :name, :width, :height, :aspect_ratio, :dpi
6
6
 
7
7
  def initialize name:, width:, height:, path: nil, url: nil, contents: nil
8
8
  @name = name
@@ -11,6 +11,9 @@ module Consolidate
11
11
  @path = path
12
12
  @url = url
13
13
  @contents = contents
14
+ @aspect_ratio = width.to_f / height.to_f
15
+ #  TODO: Read this from the contents
16
+ @dpi = {x: 72, y: 72}
14
17
  end
15
18
 
16
19
  def to_s = name
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Consolidate
4
- VERSION = "0.3.9"
4
+ VERSION = "0.4.1"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: standard-procedure-consolidate
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.9
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rahoul Baruah
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-12-04 00:00:00.000000000 Z
11
+ date: 2024-12-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rubyzip
@@ -64,6 +64,8 @@ files:
64
64
  - checksums/standard-procedure-consolidate-0.3.0.gem.sha512
65
65
  - checksums/standard-procedure-consolidate-0.3.1.gem.sha512
66
66
  - checksums/standard-procedure-consolidate-0.3.9.gem.sha512
67
+ - checksums/standard-procedure-consolidate-0.4.0.gem.sha512
68
+ - checksums/standard-procedure-consolidate-0.4.1.gem.sha512
67
69
  - exe/consolidate
68
70
  - exe/examine
69
71
  - lib/consolidate.rb