You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
329 lines
9.1 KiB
CoffeeScript
329 lines
9.1 KiB
CoffeeScript
LineWrapper = require '../line_wrapper'
|
|
{number} = require '../object'
|
|
|
|
module.exports =
|
|
initText: ->
|
|
# Current coordinates
|
|
@x = 0
|
|
@y = 0
|
|
@_lineGap = 0
|
|
|
|
lineGap: (@_lineGap) ->
|
|
return this
|
|
|
|
moveDown: (lines = 1) ->
|
|
@y += @currentLineHeight(true) * lines + @_lineGap
|
|
return this
|
|
|
|
moveUp: (lines = 1) ->
|
|
@y -= @currentLineHeight(true) * lines + @_lineGap
|
|
return this
|
|
|
|
_text: (text, x, y, options, lineCallback) ->
|
|
options = @_initOptions(x, y, options)
|
|
|
|
# Convert text to a string
|
|
text = '' + text
|
|
|
|
# if the wordSpacing option is specified, remove multiple consecutive spaces
|
|
if options.wordSpacing
|
|
text = text.replace(/\s{2,}/g, ' ')
|
|
|
|
# word wrapping
|
|
#console.log("options.width=" + options.width)
|
|
if options.width
|
|
wrapper = @_wrapper
|
|
unless wrapper
|
|
wrapper = new LineWrapper(this, options)
|
|
wrapper.on 'line', lineCallback
|
|
|
|
@_wrapper = if options.continued then wrapper else null
|
|
@_textOptions = if options.continued then options else null
|
|
wrapper.wrap text, options
|
|
|
|
# render paragraphs as single lines
|
|
else
|
|
#console.log("line callback!")
|
|
lineCallback line, options for line in text.split '\n'
|
|
|
|
return this
|
|
|
|
text: (text, x, y, options) ->
|
|
@_text text, x, y, options, @_line.bind(this)
|
|
|
|
widthOfString: (string, options = {}) ->
|
|
@_font.widthOfString(string, @_fontSize, options.features) + (options.characterSpacing or 0) * (string.length - 1)
|
|
|
|
heightOfString: (text, options = {}) ->
|
|
{x,y} = this
|
|
|
|
options = @_initOptions(options)
|
|
options.height = Infinity # don't break pages
|
|
|
|
lineGap = options.lineGap or @_lineGap or 0
|
|
@_text text, @x, @y, options, (line, options) =>
|
|
@y += @currentLineHeight(true) + lineGap
|
|
|
|
height = @y - y
|
|
@x = x
|
|
@y = y
|
|
|
|
return height
|
|
|
|
list: (list, x, y, options, wrapper) ->
|
|
options = @_initOptions(x, y, options)
|
|
|
|
midLine = Math.round (@_font.ascender / 1000 * @_fontSize) / 2
|
|
r = options.bulletRadius or Math.round (@_font.ascender / 1000 * @_fontSize) / 3
|
|
indent = options.textIndent or r * 5
|
|
itemIndent = options.bulletIndent or r * 8
|
|
|
|
level = 1
|
|
items = []
|
|
levels = []
|
|
|
|
flatten = (list) ->
|
|
for item, i in list
|
|
if Array.isArray(item)
|
|
level++
|
|
flatten(item)
|
|
level--
|
|
else
|
|
items.push(item)
|
|
levels.push(level)
|
|
|
|
flatten(list)
|
|
|
|
wrapper = new LineWrapper(this, options)
|
|
wrapper.on 'line', @_line.bind(this)
|
|
|
|
level = 1
|
|
i = 0
|
|
wrapper.on 'firstLine', =>
|
|
if (l = levels[i++]) isnt level
|
|
diff = itemIndent * (l - level)
|
|
@x += diff
|
|
wrapper.lineWidth -= diff
|
|
level = l
|
|
|
|
@circle @x - indent + r, @y + midLine, r
|
|
@fill()
|
|
|
|
wrapper.on 'sectionStart', =>
|
|
pos = indent + itemIndent * (level - 1)
|
|
@x += pos
|
|
wrapper.lineWidth -= pos
|
|
|
|
wrapper.on 'sectionEnd', =>
|
|
pos = indent + itemIndent * (level - 1)
|
|
@x -= pos
|
|
wrapper.lineWidth += pos
|
|
|
|
wrapper.wrap items.join('\n'), options
|
|
|
|
return this
|
|
|
|
_initOptions: (x = {}, y, options = {}) ->
|
|
if typeof x is 'object'
|
|
options = x
|
|
x = null
|
|
|
|
# clone options object
|
|
options = do ->
|
|
opts = {}
|
|
opts[k] = v for k, v of options
|
|
return opts
|
|
|
|
# extend options with previous values for continued text
|
|
if @_textOptions
|
|
for key, val of @_textOptions when key isnt 'continued'
|
|
options[key] ?= val
|
|
|
|
# Update the current position
|
|
if x?
|
|
@x = x
|
|
if y?
|
|
@y = y
|
|
|
|
# wrap to margins if no x or y position passed
|
|
unless options.lineBreak is false
|
|
margins = @page.margins
|
|
options.width ?= @page.width - @x - margins.right
|
|
|
|
options.columns ||= 0
|
|
options.columnGap ?= 18 # 1/4 inch
|
|
|
|
return options
|
|
|
|
_line: (text, options = {}, wrapper) ->
|
|
#console.log("in _line!")
|
|
@_fragment text, @x, @y, options
|
|
lineGap = options.lineGap or @_lineGap or 0
|
|
|
|
if not wrapper
|
|
@x += @widthOfString text
|
|
else
|
|
@y += @currentLineHeight(true) + lineGap
|
|
|
|
_fragment: (text, x, y, options) ->
|
|
text = ('' + text).replace(/\n/g, '')
|
|
return if text.length is 0
|
|
|
|
# handle options
|
|
align = options.align or 'left'
|
|
wordSpacing = options.wordSpacing or 0
|
|
characterSpacing = options.characterSpacing or 0
|
|
|
|
# text alignments
|
|
if options.width
|
|
switch align
|
|
when 'right'
|
|
textWidth = @widthOfString text.replace(/\s+$/, ''), options
|
|
x += options.lineWidth - textWidth
|
|
|
|
when 'center'
|
|
x += options.lineWidth / 2 - options.textWidth / 2
|
|
|
|
when 'justify'
|
|
# calculate the word spacing value
|
|
words = text.trim().split(/\s+/)
|
|
textWidth = @widthOfString(text.replace(/\s+/g, ''), options)
|
|
spaceWidth = @widthOfString(' ') + characterSpacing
|
|
wordSpacing = Math.max 0, (options.lineWidth - textWidth) / Math.max(1, words.length - 1) - spaceWidth
|
|
|
|
#console.log("momo me" + options.textWidth)
|
|
|
|
# calculate the actual rendered width of the string after word and character spacing
|
|
|
|
renderedWidth = (options.textWidth || @widthOfString(text, options)) + (wordSpacing * ((options.wordCount || 0) - 1)) + (characterSpacing * (text.length - 1))
|
|
|
|
# create link annotations if the link option is given
|
|
if options.link
|
|
@link x, y, renderedWidth, @currentLineHeight(), options.link
|
|
|
|
#console.log("mama me")
|
|
|
|
# create underline or strikethrough line
|
|
if options.underline or options.strike
|
|
console.log("enter underline")
|
|
@save()
|
|
@strokeColor @_fillColor... unless options.stroke
|
|
|
|
lineWidth = if @_fontSize < 10 then 0.5 else Math.floor(@_fontSize / 10)
|
|
@lineWidth lineWidth
|
|
|
|
#console.log("lineWidth" + lineWidth)
|
|
|
|
d = if options.underline then 1 else 2
|
|
lineY = y + @currentLineHeight() / d
|
|
lineY -= lineWidth if options.underline
|
|
|
|
@moveTo x, lineY
|
|
@lineTo x + renderedWidth, lineY
|
|
@stroke()
|
|
@restore()
|
|
|
|
# flip coordinate system
|
|
@save()
|
|
@transform 1, 0, 0, -1, 0, @page.height
|
|
y = @page.height - y - (@_font.ascender / 1000 * @_fontSize)
|
|
|
|
# add current font to page if necessary
|
|
@page.fonts[@_font.id] ?= @_font.ref()
|
|
|
|
#console.log("mercy me")
|
|
|
|
# begin the text object
|
|
@addContent "BT"
|
|
|
|
# text position
|
|
@addContent "1 0 0 1 #{number(x)} #{number(y)} Tm"
|
|
|
|
# font and font size
|
|
@addContent "/#{@_font.id} #{number(@_fontSize)} Tf"
|
|
|
|
# rendering mode
|
|
mode = if options.fill and options.stroke then 2 else if options.stroke then 1 else 0
|
|
@addContent "#{mode} Tr" if mode
|
|
|
|
# Character spacing
|
|
@addContent "#{number(characterSpacing)} Tc" if characterSpacing
|
|
|
|
# Add the actual text
|
|
# If we have a word spacing value, we need to encode each word separately
|
|
# since the normal Tw operator only works on character code 32, which isn't
|
|
# used for embedded fonts.
|
|
#console.log("wordSpacing="+wordSpacing)
|
|
if wordSpacing
|
|
words = text.trim().split(/\s+/)
|
|
wordSpacing += @widthOfString(' ') + characterSpacing
|
|
wordSpacing *= 1000 / @_fontSize
|
|
|
|
encoded = []
|
|
positions = []
|
|
for word in words
|
|
[encodedWord, positionsWord] = @_font.encode(word, options.features)
|
|
encoded.push encodedWord...
|
|
positions.push positionsWord...
|
|
|
|
# add the word spacing to the end of the word
|
|
positions[positions.length - 1].xAdvance += wordSpacing
|
|
else
|
|
[encoded, positions] = @_font.encode(text, options.features)
|
|
|
|
scale = @_fontSize / 1000
|
|
commands = []
|
|
last = 0
|
|
hadOffset = no
|
|
|
|
# Adds a segment of text to the TJ command buffer
|
|
addSegment = (cur) =>
|
|
if last < cur
|
|
hex = encoded.slice(last, cur).join ''
|
|
advance = positions[cur - 1].xAdvance - positions[cur - 1].advanceWidth
|
|
commands.push "<#{hex}> #{number(-advance)}"
|
|
|
|
last = cur
|
|
|
|
# Flushes the current TJ commands to the output stream
|
|
flush = (i) =>
|
|
addSegment i
|
|
|
|
if commands.length > 0
|
|
@addContent "[#{commands.join ' '}] TJ"
|
|
commands.length = 0
|
|
|
|
for pos, i in positions
|
|
# If we have an x or y offset, we have to break out of the current TJ command
|
|
# so we can move the text position.
|
|
#console.log("pos.xOffset or pos.yOffset=" + (pos.xOffset or pos.yOffset))
|
|
if pos.xOffset or pos.yOffset
|
|
# Flush the current buffer
|
|
flush i
|
|
|
|
# Move the text position and flush just the current character
|
|
@addContent "1 0 0 1 #{number(x + pos.xOffset * scale)} #{number(y + pos.yOffset * scale)} Tm"
|
|
flush i + 1
|
|
|
|
hadOffset = yes
|
|
else
|
|
# If the last character had an offset, reset the text position
|
|
if hadOffset
|
|
@addContent "1 0 0 1 #{number(x)} #{number(y)} Tm"
|
|
hadOffset = no
|
|
|
|
# Group segments that don't have any advance adjustments
|
|
unless pos.xAdvance - pos.advanceWidth is 0
|
|
addSegment i + 1
|
|
|
|
x += pos.xAdvance * scale
|
|
|
|
# Flush any remaining commands
|
|
flush i
|
|
|
|
# end the text object
|
|
@addContent "ET"
|
|
|
|
# restore flipped coordinate system
|
|
@restore()
|