diff --git a/creator/paragraph.go b/creator/paragraph.go index 1d308d5a..f015824a 100644 --- a/creator/paragraph.go +++ b/creator/paragraph.go @@ -186,10 +186,7 @@ func (p *Paragraph) Width() float64 { // Height returns the height of the Paragraph. The height is calculated based on the input text and // how it is wrapped within the container. Does not include Margins. func (p *Paragraph) Height() float64 { - if p.textLines == nil || len(p.textLines) == 0 { - p.wrapText() - } - + p.wrapText() return float64(len(p.textLines)) * p.lineHeight * p.fontSize } diff --git a/creator/styled_paragraph.go b/creator/styled_paragraph.go index 02ed8195..f01680db 100644 --- a/creator/styled_paragraph.go +++ b/creator/styled_paragraph.go @@ -206,9 +206,7 @@ func (p *StyledParagraph) Width() float64 { // Height returns the height of the Paragraph. The height is calculated based on the input text and how it is wrapped // within the container. Does not include Margins. func (p *StyledParagraph) Height() float64 { - if p.lines == nil || len(p.lines) == 0 { - p.wrapText() - } + p.wrapText() var height float64 for _, line := range p.lines { @@ -296,7 +294,7 @@ func (p *StyledParagraph) getTextWidth() float64 { width += style.FontSize * metrics.Wx // Do not add character spacing for the last character of the line. - if i != lenChunks-1 || j != lenRunes-1 { + if r != ' ' && (i != lenChunks-1 || j != lenRunes-1) { width += style.CharSpacing * 1000.0 } } @@ -331,7 +329,7 @@ func (p *StyledParagraph) getTextLineWidth(line []*TextChunk) float64 { width += style.FontSize * metrics.Wx // Do not add character spacing for the last character of the line. - if i != lenChunks-1 || j != lenRunes-1 { + if r != ' ' && (i != lenChunks-1 || j != lenRunes-1) { width += style.CharSpacing * 1000.0 } } @@ -425,15 +423,19 @@ func (p *StyledParagraph) wrapText() error { widths = nil continue } + isSpace := r == ' ' metrics, found := style.Font.GetRuneMetrics(r) if !found { common.Log.Debug("Rune char metrics not found! %v\n", r) return errors.New("glyph char metrics missing") } - w := style.FontSize * metrics.Wx - charWidth := w + style.CharSpacing*1000.0 + + charWidth := w + if !isSpace { + charWidth = w + style.CharSpacing*1000.0 + } if lineWidth+w > p.wrapWidth*1000.0 { // Goes out of bounds: Wrap. @@ -441,10 +443,12 @@ func (p *StyledParagraph) wrapText() error { // TODO: when goes outside: back up to next space, // otherwise break on the character. idx := -1 - for j := len(part) - 1; j >= 0; j-- { - if part[j] == ' ' { - idx = j - break + if !isSpace { + for j := len(part) - 1; j >= 0; j-- { + if part[j] == ' ' { + idx = j + break + } } } @@ -462,9 +466,15 @@ func (p *StyledParagraph) wrapText() error { lineWidth += width } } else { - lineWidth = charWidth - part = []rune{r} - widths = []float64{charWidth} + if isSpace { + lineWidth = 0 + part = []rune{} + widths = []float64{} + } else { + lineWidth = charWidth + part = []rune{r} + widths = []float64{charWidth} + } } line = append(line, &TextChunk{ @@ -589,13 +599,19 @@ func drawStyledParagraphOnBlock(blk *Block, p *StyledParagraph, ctx DrawContext) // Add the fonts of all chunks to the page resources. var fonts [][]core.PdfObjectName - for _, line := range p.lines { + var yOffset float64 + for i, line := range p.lines { var fontLine []core.PdfObjectName for _, chunk := range line { + style := chunk.Style + if i == 0 && style.FontSize > yOffset { + yOffset = style.FontSize + } + fontName = core.PdfObjectName(fmt.Sprintf("Font%d", num)) - err := blk.resources.SetFontByName(fontName, chunk.Style.Font.ToPdfObject()) + err := blk.resources.SetFontByName(fontName, style.Font.ToPdfObject()) if err != nil { return ctx, err } @@ -611,7 +627,7 @@ func drawStyledParagraphOnBlock(blk *Block, p *StyledParagraph, ctx DrawContext) cc := contentstream.NewContentCreator() cc.Add_q() - yPos := ctx.PageHeight - ctx.Y - defaultFontSize*p.lineHeight + yPos := ctx.PageHeight - ctx.Y - yOffset*p.lineHeight cc.Translate(ctx.X, yPos) if p.angle != 0 { diff --git a/creator/styled_paragraph_test.go b/creator/styled_paragraph_test.go index 9829b4b1..3e8af20e 100644 --- a/creator/styled_paragraph_test.go +++ b/creator/styled_paragraph_test.go @@ -8,6 +8,7 @@ package creator import ( "testing" + "github.com/stretchr/testify/require" "github.com/unidoc/unipdf/v3/model" ) @@ -861,3 +862,26 @@ func TestStyledParagraphTableVerticalAlignment(t *testing.T) { // Write output file. testWriteAndRender(t, c, "styled_paragraph_table_vertical_align.pdf") } + +func TestStyledParagraphCharacterSpaceWrapping(t *testing.T) { + c := New() + var posX, posY, width float64 = 10, 10, 120 + + // Draw paragraph. + p := c.NewStyledParagraph() + p.SetPos(posX, posY) + p.SetWidth(width) + + chunk := p.Append("s o m e t e x t s o m e t e x t s o m e t e x t s o m e t e x t s o m e t e x t") + chunk.Style.FontSize = 8 + chunk.Style.CharSpacing = 5 + require.NoError(t, c.Draw(p)) + + // Draw border. + border := c.NewRectangle(posX, posY, p.Width(), p.Height()) + border.SetBorderColor(ColorRed) + require.NoError(t, c.Draw(border)) + + // Write output file. + testWriteAndRender(t, c, "styled_paragraph_character_space_wrapping.pdf") +} diff --git a/creator/table.go b/creator/table.go index f2d502fc..601e3904 100644 --- a/creator/table.go +++ b/creator/table.go @@ -540,7 +540,7 @@ func (table *Table) GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawContext, } // Account for the top offset the paragraph adds. - vertOffset = lineCapHeight - t.defaultStyle.FontSize*t.lineHeight + vertOffset = lineCapHeight - lineHeight switch cell.verticalAlignment { case CellVerticalAlignmentTop: diff --git a/creator/text_chunk.go b/creator/text_chunk.go index 32e3cf2f..bd5b0569 100644 --- a/creator/text_chunk.go +++ b/creator/text_chunk.go @@ -68,6 +68,7 @@ func (tc *TextChunk) Wrap(width float64) ([]string, error) { widths = nil continue } + isSpace := r == ' ' metrics, found := style.Font.GetRuneMetrics(r) if !found { @@ -77,21 +78,29 @@ func (tc *TextChunk) Wrap(width float64) ([]string, error) { common.Log.Trace("Encoder: %#v", style.Font.Encoder()) return nil, errors.New("glyph char metrics missing") } - w := style.FontSize * metrics.Wx - charWidth := w + style.CharSpacing*1000.0 + + charWidth := w + if !isSpace { + charWidth = w + style.CharSpacing*1000.0 + } + if lineWidth+w > width*1000.0 { // Goes out of bounds. Break on the character. idx := -1 - for i := len(line) - 1; i >= 0; i-- { - if line[i] == ' ' { - idx = i - break + if !isSpace { + for i := len(line) - 1; i >= 0; i-- { + if line[i] == ' ' { + idx = i + break + } } } + + text := string(line) if idx > 0 { // Back up to last space. - lines = append(lines, strings.TrimRightFunc(string(line[0:idx+1]), unicode.IsSpace)) + text = string(line[0 : idx+1]) // Remainder of line. line = append(line[idx+1:], r) @@ -102,11 +111,18 @@ func (tc *TextChunk) Wrap(width float64) ([]string, error) { lineWidth += width } } else { - lines = append(lines, strings.TrimRightFunc(string(line), unicode.IsSpace)) - line = []rune{r} - widths = []float64{charWidth} - lineWidth = charWidth + if isSpace { + line = []rune{} + widths = []float64{} + lineWidth = 0 + } else { + line = []rune{r} + widths = []float64{charWidth} + lineWidth = charWidth + } } + + lines = append(lines, strings.TrimRightFunc(text, unicode.IsSpace)) } else { line = append(line, r) lineWidth += charWidth diff --git a/creator/text_chunk_test.go b/creator/text_chunk_test.go index 3a1952be..b41c8010 100644 --- a/creator/text_chunk_test.go +++ b/creator/text_chunk_test.go @@ -58,8 +58,8 @@ func TestTextChunkWrap(t *testing.T) { "irure dolor in", "reprehenderit in", "voluptate velit esse", - "cillum dolore eu", - "fugiat nulla pariatur.", + "cillum dolore eu fugiat", + "nulla pariatur.", "Excepteur sint", "occaecat cupidatat", "non proident, sunt in",