Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docx gem 支持写入样式 #36

Open
mxchenxiaodong opened this issue Jun 19, 2022 · 0 comments
Open

docx gem 支持写入样式 #36

mxchenxiaodong opened this issue Jun 19, 2022 · 0 comments

Comments

@mxchenxiaodong
Copy link
Owner

1、背景

一开始的需求,是要支持写入的文字能够带样式。
那首先,得知道基本的结构吧?
有点懵……

文档说明貌似没怎么找得到,直接从代码下手吧~

2、处理

先创建一个最简单的 docx 文件。
hello.png
先简单过一下 docx 项目的整体结构。从 document.rb 文件入手,来看看是什么样的数据结构。
irb中进行读取:

require 'nokogiri'
require 'zip'

zip = Zip::File.open('/Users/rcc0016748/Desktop/hello.docx')
document = zip.glob('word/document*.xml').first

# 可以拿到原始的 xml 结构
document_xml = document.get_input_stream.read

# 生成 xml 实例对象
doc = Nokogiri::XML(document_xml)

基于 zipnokogiri 这2个包。
通过 Nokogiri 生成的 xml 文档实例后,就可以对数据进行读取。
原来,docx 文件是基于 xml 格式来保存数据的啊!

Note:
nokogiri 可以用来做什么?
nokogiri 非常快速地解析和搜索XML / HTML,并实现了CSS3选择器支持以及XPath 1.0支持。

Installing Nokogiri - Nokogiri

至于生成的 xml 实例可以怎么用?
比如,段落的匹配:

doc.xpath('//w:document//w:body/w:p')

比如,书签的匹配:

@doc.xpath('//w:bookmarkStart')

匹配的数据就跟使用使用html选择器拿到的数据差不多,符合条件就会被收集在数组里。

另外,在终端查看生成的 xml 为:

<w:document xmlns:wpc="http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas"
  xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
  xmlns:o="urn:schemas-microsoft-com:office:office"
  xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
  xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"
  xmlns:v="urn:schemas-microsoft-com:vml"
  xmlns:wp14="http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing"
  xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"
  xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
  xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml"
  xmlns:w10="urn:schemas-microsoft-com:office:word"
  xmlns:w15="http://schemas.microsoft.com/office/word/2012/wordml"
  xmlns:wpg="http://schemas.microsoft.com/office/word/2010/wordprocessingGroup"
  xmlns:wpi="http://schemas.microsoft.com/office/word/2010/wordprocessingInk"
  xmlns:wne="http://schemas.microsoft.com/office/word/2006/wordml"
  xmlns:wps="http://schemas.microsoft.com/office/word/2010/wordprocessingShape"
  xmlns:wpsCustomData="http://www.wps.cn/officeDocument/2013/wpsCustomData" mc:Ignorable="w14 w15 wp14">
  <w:body>
    <w:p>
      <w:r>
        <w:rPr>
          <w:b/>
          <w:bCs/>
          <w:i/>
          <w:iCs/>
        </w:rPr>
        <w:t>你好</w:t>
      </w:r>
      <w:r>
        <w:t>。</w:t>
      </w:r>
    </w:p>
    <w:p>
      <w:pPr>
        <w:rPr>
          <w:color w:val="C55A11" w:themeColor="accent2" w:themeShade="BF"/>
        </w:rPr>
      </w:pPr>
      <w:r>
        <w:rPr>
          <w:color w:val="C55A11" w:themeColor="accent2" w:themeShade="BF"/>
          <w:u w:val="single"/>
        </w:rPr>
        <w:t>一天的时间</w:t>
      </w:r>
      <w:r>
        <w:rPr>
          <w:color w:val="C55A11" w:themeColor="accent2" w:themeShade="BF"/>
        </w:rPr>
        <w:t>。</w:t>
      </w:r>
    </w:p>
    <w:p>
      <w:bookmarkStart w:id="0" w:name="_GoBack"/>
      <w:bookmarkEnd w:id="0"/>
    </w:p>
    <w:p/>
    <w:sectPr>
      <w:pgSz w:w="11906" w:h="16838"/>
      <w:pgMar w:top="1440" w:right="1800" w:bottom="1440" w:left="1800" w:header="851" w:footer="992" w:gutter="0"/>
      <w:cols w:space="425" w:num="1"/>
      <w:docGrid w:type="lines" w:linePitch="312" w:charSpace="0"/>
    </w:sectPr>
  </w:body>
</w:document>

抛去前后一些整体页面的结构,来看看第一个段落:

<w:p>
  <w:r>   <!-- 段落中以相连续的中文或英文字符字符串,做为开始和结束。--> 
    <w:rPr>  <!-- 是<w:r>标签内的标签,对Run文本属性进行修饰- -->
      <w:b/>  <!-- 粗体 -->
      <w:bCs/>
      <w:i/>  <!-- 斜体 -->
      <w:iCs/>
    </w:rPr>
    <w:t>你好</w:t>  <!-- 表示真正的文本内容 -->
  </w:r>

  <w:r>
    <w:t>。</w:t>
  </w:r>
</w:p>

一些标签大概的意思可以参考:word xml 各个标签含义 - JavaShuo

主要意思就是:在一个  <w:r>(也叫textrun) 中,可以包含:

  • <w:rPr>包起来的各种各种属性,
  • <w:t> 真正的文本内容

再来看看这段:

<w:r>
	<w:rPr>
	  <w:color w:val="C55A11" w:themeColor="accent2" w:themeShade="BF"/>
	  <w:u w:val="single"/>
	</w:rPr>
	<w:t>一天的时间</w:t>
</w:r>

<w:rPr> 的属性值:

  • <w:color> 表示颜色属性,w:val 用来设置具体的颜色。
  • <w:u> 表示下划线属性, w:val="single" 用来设置具体的下划线类型。

好了。背景就是这样子。

那看回我们的需求,希望能够支持写入样式。那么对应的做法就是:
多加一个参数,用来接收想要的样式,然后给对应的节点 <w:r> 增加它<w:rPr> 中的各个属性:颜色、加粗、下划线、字体大小等等。

docx 中,插入文字的主要方法有:

  • insert_text_after
  • insert_text_before
  • insert_multiple_lines
  • text=  (最终都会调用到该方法)

docx gem 中,经过nokogiri 解析 xml 后, <w:r> 对应的类是 lib/docx/containers/text_run.rb

<w:r> xml 节点存于 @node 中:

<w:r>
  <w:rPr>
    <w:color w:val="C55A11" w:themeColor="accent2" w:themeShade="BF"/>
    <w:u w:val="single"/>
  </w:rPr>

  <w:t>一天的时间</w:t>
</w:r>

初始化:

module Docx
  module Elements
    module Containers
      class TextRun
        def initialize(node, document_properties = {})
          @node = node
          @document_properties = document_properties

			# 可以看到,要相关数据都可以在 @node 这个节点对象里面取。
          @text_nodes = @node.xpath('w:t|w:r/w:t').map { |t_node| Elements::Text.new(t_node) }

          @properties_tag = 'rPr'
          @text       = parse_text || ''
          @font_size = @document_properties[:font_size]
        end
      end
    end
  end
end

原先设置文本内容的方法,text=

# 设置某个 TextRun 实例的文本内容
def text=(content)
  if @text_nodes.size == 1
    @text_nodes.first.content = content
  elsif @text_nodes.empty?
    new_t = Elements::Text.create_within(self)
    new_t.content = content
  end
end

所以,目前只能插入文字,并没有样式。

现在来看看怎么支持?
我们可以添加一个方法:在  text=(content) 的基础上,再额外追加样式。

# 新增设置带样式文本的方法
def set_text(content, formatting = {})
  self.text = content
  apply_formatting(formatting)
end

formatting 的参数支持:

  • :italic => boolean
  • :bold => boolean
  • :underline => boolean
  • :font => ‘font_name’
  • :font_size => font_size, 12/ 16 / …
  • :color =>  ‘FF0000’

剩下的重任就是 apply_formatting 身上了!!!!!

再来看看这个例子,<w:r> xml 节点存于 @node 中:

<w:r>
  <w:rPr>                        <-- 用来表示属性的集合,开始
    <w:color w:val="C55A11" />     <-- 颜色属性
    <w:u w:val="single"/>          <-- 下划线属性
  </w:rPr>                       <-- 用来表示属性的集合,结束

  <w:t>一天的时间</w:t>            <-- 文本
</w:r>

大概思路为:

  • @node 中,搜索一下有没有 <w:rPr>,没有的话,需要创建。
  • 根据传入的 formatting ,在 <w:rPr> 中追加具体的属性。
# 給节点添加属性
module Docx
  module Formatting
    # 找到当前节点 node 下的属性集合节点 -- -- rPr
    def properties_node
      properties = node.at_xpath(".//w:rPr")
      properties ||= node.prepend_child("<w:rPr></w:rPr>").first
    end
		
    # 添加具体的属性值。一旦调用,就会追加进对应的 rPr 中了。
    # tag 比如为:
    # - color
    # - u
    def add_property(tag)
      properties_node.remove if properties_node.at_xpath(".//w:#{tag}")
      properties_node.add_child("<w:#{tag}/>").first
    end
  end
end

那现在来看看 apply_formatting :

def apply_formatting(formatting)
  # 是否斜体
  add_property('i') if formatting[:italic]
  
  # 是否加粗
  add_property('b') if formatting[:bold]

  # 添加下划线,需要指定 val
  if formatting[:underline]
    underline_node = add_property('u')
    underline_node["w:val"] = 'single'
  end

  if (formatting[:color])
    color_node = add_property('color')
    color_node["w:val"] = formatting[:color]
  end
end

3、思路总结

  • docx 文件,是以 xml 结构进行存储。
  • 通过  zip 进行文件读取, 用 nokogiri 进行 xml 的解析,生成对应的节点。
  • 一个文本节点(叫做 text_run),用 <w:r> 包起来,里面包含 <w:rPr> 属性节点 + <w:t> 内容节点。
  • 为插入文字添加样式时,操作的是 <w:rPr> 下的具体属性(斜体、加粗、下划线等)

通过此次改动,学习到了 docx 文档存储方式与相关处理。

4、其他

这个需求,是尽量让业务方减少改动,尽量扩展。
假如扩展不了的话,也可以通话其他方案去实现,比如单独使用相关的服务,通过接口调用的方式不再基于 docx 做操作等等。

此次调整参考一个好几年前未合的merge request: https://github.com/ruby-docx/docx/pull/26,做了相关的改动。

5、文章

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant