/lib/mixins/text.coffee

http://github.com/devongovett/pdfkit · CoffeeScript · 269 lines · 173 code · 63 blank · 33 comment · 35 complexity · 3d432bb264f491bd8280a793d4be1707 MD5 · raw file

  1. LineWrapper = require '../line_wrapper'
  2. module.exports =
  3. initText: ->
  4. # Current coordinates
  5. @x = 0
  6. @y = 0
  7. @_lineGap = 0
  8. lineGap: (@_lineGap) ->
  9. return this
  10. moveDown: (lines = 1) ->
  11. @y += @currentLineHeight(true) * lines + @_lineGap
  12. return this
  13. moveUp: (lines = 1) ->
  14. @y -= @currentLineHeight(true) * lines + @_lineGap
  15. return this
  16. _text: (text, x, y, options, lineCallback) ->
  17. options = @_initOptions(x, y, options)
  18. # Convert text to a string
  19. text = '' + text
  20. # if the wordSpacing option is specified, remove multiple consecutive spaces
  21. if options.wordSpacing
  22. text = text.replace(/\s{2,}/g, ' ')
  23. # word wrapping
  24. if options.width
  25. wrapper = @_wrapper
  26. unless wrapper
  27. wrapper = new LineWrapper(this, options)
  28. wrapper.on 'line', lineCallback
  29. @_wrapper = if options.continued then wrapper else null
  30. @_textOptions = if options.continued then options else null
  31. wrapper.wrap text, options
  32. # render paragraphs as single lines
  33. else
  34. lineCallback line, options for line in text.split '\n'
  35. return this
  36. text: (text, x, y, options) ->
  37. @_text text, x, y, options, @_line.bind(this)
  38. widthOfString: (string, options = {}) ->
  39. @_font.widthOfString(string, @_fontSize) + (options.characterSpacing or 0) * (string.length - 1)
  40. heightOfString: (text, options = {}) ->
  41. {x,y} = this
  42. options = @_initOptions(options)
  43. options.height = Infinity # don't break pages
  44. lineGap = options.lineGap or @_lineGap or 0
  45. @_text text, @x, @y, options, (line, options) =>
  46. @y += @currentLineHeight(true) + lineGap
  47. height = @y - y
  48. @x = x
  49. @y = y
  50. return height
  51. list: (list, x, y, options, wrapper) ->
  52. options = @_initOptions(x, y, options)
  53. r = Math.round (@_font.ascender / 1000 * @_fontSize) / 3
  54. indent = options.textIndent or r * 5
  55. itemIndent = options.bulletIndent or r * 8
  56. level = 1
  57. items = []
  58. levels = []
  59. flatten = (list) ->
  60. for item, i in list
  61. if Array.isArray(item)
  62. level++
  63. flatten(item)
  64. level--
  65. else
  66. items.push(item)
  67. levels.push(level)
  68. flatten(list)
  69. wrapper = new LineWrapper(this, options)
  70. wrapper.on 'line', @_line.bind(this)
  71. level = 1
  72. i = 0
  73. wrapper.on 'firstLine', =>
  74. if (l = levels[i++]) isnt level
  75. diff = itemIndent * (l - level)
  76. @x += diff
  77. wrapper.lineWidth -= diff
  78. level = l
  79. @circle @x - indent + r, @y + r + (r / 2), r
  80. @fill()
  81. wrapper.on 'sectionStart', =>
  82. pos = indent + itemIndent * (level - 1)
  83. @x += pos
  84. wrapper.lineWidth -= pos
  85. wrapper.on 'sectionEnd', =>
  86. pos = indent + itemIndent * (level - 1)
  87. @x -= pos
  88. wrapper.lineWidth += pos
  89. wrapper.wrap items.join('\n'), options
  90. return this
  91. _initOptions: (x = {}, y, options = {}) ->
  92. if typeof x is 'object'
  93. options = x
  94. x = null
  95. # clone options object
  96. options = do ->
  97. opts = {}
  98. opts[k] = v for k, v of options
  99. return opts
  100. # extend options with previous values for continued text
  101. if @_textOptions
  102. for key, val of @_textOptions when key isnt 'continued'
  103. options[key] ?= val
  104. # Update the current position
  105. if x?
  106. @x = x
  107. if y?
  108. @y = y
  109. # wrap to margins if no x or y position passed
  110. unless options.lineBreak is false
  111. margins = @page.margins
  112. options.width ?= @page.width - @x - margins.right
  113. options.columns ||= 0
  114. options.columnGap ?= 18 # 1/4 inch
  115. return options
  116. _line: (text, options = {}, wrapper) ->
  117. @_fragment text, @x, @y, options
  118. lineGap = options.lineGap or @_lineGap or 0
  119. if not wrapper
  120. @x += @widthOfString text
  121. else
  122. @y += @currentLineHeight(true) + lineGap
  123. _fragment: (text, x, y, options) ->
  124. text = '' + text
  125. return if text.length is 0
  126. # handle options
  127. align = options.align or 'left'
  128. wordSpacing = options.wordSpacing or 0
  129. characterSpacing = options.characterSpacing or 0
  130. # text alignments
  131. if options.width
  132. switch align
  133. when 'right'
  134. textWidth = @widthOfString text.replace(/\s+$/, ''), options
  135. x += options.lineWidth - textWidth
  136. when 'center'
  137. x += options.lineWidth / 2 - options.textWidth / 2
  138. when 'justify'
  139. # calculate the word spacing value
  140. words = text.trim().split(/\s+/)
  141. textWidth = @widthOfString(text.replace(/\s+/g, ''), options)
  142. spaceWidth = @widthOfString(' ') + characterSpacing
  143. wordSpacing = Math.max 0, (options.lineWidth - textWidth) / Math.max(1, words.length - 1) - spaceWidth
  144. # calculate the actual rendered width of the string after word and character spacing
  145. renderedWidth = options.textWidth + (wordSpacing * (options.wordCount - 1)) + (characterSpacing * (text.length - 1))
  146. # create link annotations if the link option is given
  147. if options.link
  148. @link x, y, renderedWidth, @currentLineHeight(), options.link
  149. # create underline or strikethrough line
  150. if options.underline or options.strike
  151. @save()
  152. @strokeColor @_fillColor... unless options.stroke
  153. lineWidth = if @_fontSize < 10 then 0.5 else Math.floor(@_fontSize / 10)
  154. @lineWidth lineWidth
  155. d = if options.underline then 1 else 2
  156. lineY = y + @currentLineHeight() / d
  157. lineY -= lineWidth if options.underline
  158. @moveTo x, lineY
  159. @lineTo x + renderedWidth, lineY
  160. @stroke()
  161. @restore()
  162. # flip coordinate system
  163. @save()
  164. @transform 1, 0, 0, -1, 0, @page.height
  165. y = @page.height - y - (@_font.ascender / 1000 * @_fontSize)
  166. # add current font to page if necessary
  167. @page.fonts[@_font.id] ?= @_font.ref()
  168. # tell the font subset to use the characters
  169. @_font.use(text)
  170. # begin the text object
  171. @addContent "BT"
  172. # text position
  173. @addContent "#{x} #{y} Td"
  174. # font and font size
  175. @addContent "/#{@_font.id} #{@_fontSize} Tf"
  176. # rendering mode
  177. mode = if options.fill and options.stroke then 2 else if options.stroke then 1 else 0
  178. @addContent "#{mode} Tr" if mode
  179. # Character spacing
  180. @addContent "#{characterSpacing} Tc" if characterSpacing
  181. # Add the actual text
  182. # If we have a word spacing value, we need to encode each word separately
  183. # since the normal Tw operator only works on character code 32, which isn't
  184. # used for embedded fonts.
  185. if wordSpacing
  186. words = text.trim().split(/\s+/)
  187. wordSpacing += @widthOfString(' ') + characterSpacing
  188. wordSpacing *= 1000 / @_fontSize
  189. commands = []
  190. for word in words
  191. # encode the text based on the font subset,
  192. # and then convert it to hex
  193. encoded = @_font.encode(word)
  194. encoded = (encoded.charCodeAt(i).toString(16) for i in [0...encoded.length] by 1).join('')
  195. commands.push "<#{encoded}> #{-wordSpacing}"
  196. @addContent "[#{commands.join ' '}] TJ"
  197. else
  198. # encode the text based on the font subset,
  199. # and then convert it to hex
  200. encoded = @_font.encode(text)
  201. encoded = (encoded.charCodeAt(i).toString(16) for i in [0...encoded.length] by 1).join('')
  202. @addContent "<#{encoded}> Tj"
  203. # end the text object
  204. @addContent "ET"
  205. # restore flipped coordinate system
  206. @restore()