maroto icon indicating copy to clipboard operation
maroto copied to clipboard

Line overlap when using long text

Open realvasko opened this issue 3 years ago • 15 comments

Describe the bug When adding text cells with long text and extrapolation is set to false, the PDF will have line breaks in a way that the text fits on the page. Since the row height has to be provided by the user, it's not possible to know what size to pick. As a result, the next row cell will start at the position where it should be if there were no line breaks.

Looking at the code, in text.go the Add() function correctly calculates the accumulateOffsetY value to render each line within the cell. That value somehow needs to be passed to the row, but I don't see a way to retrieve it.

Since the PDF is generated dynamically based on user templates, it's not possible to know how long each text line will be, this has to be done programatically.

To Reproduce The error can be reproduced by running below code.

Related code:


var longLine = `Sed ut perspiciatis, unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam eaque ipsa, quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt, explicabo. Nemo enim ipsam voluptatem, quia voluptas sit, aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos, qui ratione voluptatem sequi nesciunt, neque porro quisquam est, qui dolorem ipsum, quia dolor sit amet consectetur adipisci[ng] velit, sed quia non numquam [do] eius modi tempora inci[di]dunt, ut labore et dolore magnam aliquam quaerat voluptatem.`

func main() {
	m := pdf.NewMaroto(consts.Portrait, consts.A4)
	m.SetPageMargins(20, 10, 20)

	for i := 0; i < 5; i++ {
		m.Row(10, func() {
			m.Col(12, func() {
				m.Text(longLine, props.Text{
					Top:         0,
					Size:        12,
					Extrapolate: false,
				})
			})
		})
	}

	err := m.OutputFileAndClose("text-overlap.pdf")
	if err != nil {
		fmt.Printf("could not save PDF: %v\n", err)
		os.Exit(1)
	}

	fmt.Println("PDF saved successfully")
}

Expected behavior I would expect each new text row cell to begin below the last line of the previous row cell.

realvasko avatar Sep 23 '22 09:09 realvasko

So what you ended up doing?

joeyave avatar Feb 09 '23 08:02 joeyave

I ended up using the underlying pdf library. In there I had to split the text into words, figure out how many words fit in a line and basically render one line after another with the words that fit in there.

realvasko avatar Feb 10 '23 09:02 realvasko

I've created the following helper function to calculate row height based on content in case it's useful to someone:

func calcRowHeight(m pdf.Maroto, text string, textProp props.Text, gridWidth, colWidth uint) float64 {
	pdfM := m.(*pdf.PdfMaroto)
	percent := float64(colWidth) / float64(gridWidth)
	pageWidth, _ := pdfM.Pdf.GetPageSize()
	left, _, right, _ := pdfM.Pdf.GetMargins()
	width := (pageWidth - right - left) * percent
	lines := 1.0
	if !textProp.Extrapolate {
		lines = float64(pdfM.TextHelper.GetLinesQuantity(text, textProp, width))
	}
	fontHeight := textProp.Size/pdfM.Font.GetScaleFactor() + textProp.VerticalPadding
	return lines*fontHeight + textProp.Top
}

@johnferchermeli would be great to have some sort of RowProp to be able to "expand" the height past the specified height using similar logic. Feels like a pretty common use case.

timoha avatar Mar 21 '23 21:03 timoha

@F-Amaral, this would be solved with that feature of automatic row height calculation

johnfercher avatar Sep 19 '23 18:09 johnfercher

When a text will be added and the height of the text extrapolate the current height it should also create a new page. I think that is gona work the v2 code automatically. Issue #128

johnfercher avatar Sep 19 '23 18:09 johnfercher

I tried to implement this on the v2 fork, but it didn't work

I don't understand how a text component can move the line height cursor because the cell is passed inside the Render methods by value, not by reference

RuSPanzer avatar Sep 20 '23 18:09 RuSPanzer

This is not implemented already on v2. We only added this to roadmap. The idea here is when you create the tree structure as something like:

Row 1
    Col 1
        Image 1
    Col 2
        Text 2
    Col 3
        Barcode 3

Every component will have the method GetHeight() and them when the someone create a Row without defining a height, it will call the GetHeight of children components recursively and the response will be always the greater height from all childrens. We will use this max height as the height of the row, in this case the row height will always match perfect to the greater components inside all cols.

johnfercher avatar Sep 20 '23 18:09 johnfercher

At row component, before render it self and before pass the values to children, it will call this get height and use the value to render it self and pass to children.

johnfercher avatar Sep 20 '23 18:09 johnfercher

This is not implemented already on v2. We only added this to roadmap. The idea here is when you create the tree structure as something like:

Row 1
    Col 1
        Image 1
    Col 2
        Text 2
    Col 3
        Barcode 3

Every component will have the method GetHeight() and them when the someone create a Row without defining a height, it will call the GetHeight of children components recursively and the response will be always the greater height from all childrens. We will use this max height as the height of the row, in this case the row height will always match perfect to the greater components inside all cols.

I wanted to help with the implementation, and began to understand the architecture. thanks for the clarification, I'll try to come up with something

RuSPanzer avatar Sep 20 '23 18:09 RuSPanzer

This is not implemented already on v2. We only added this to roadmap. The idea here is when you create the tree structure as something like:

Row 1
    Col 1
        Image 1
    Col 2
        Text 2
    Col 3
        Barcode 3

Every component will have the method GetHeight() and them when the someone create a Row without defining a height, it will call the GetHeight of children components recursively and the response will be always the greater height from all childrens. We will use this max height as the height of the row, in this case the row height will always match perfect to the greater components inside all cols.

I wanted to help with the implementation, and began to understand the architecture. thanks for the clarification, I'll try to come up with something

Thanks for your help :D, feel free to bring an implementation to this. Any doubt just send here.

johnfercher avatar Sep 20 '23 18:09 johnfercher

the problem is that to calculate the height of multiline text we need a provider since it contains parameters to calculate the actual height, such as font size or ScaleFactor

This means that we don't know the size before calling the render method. and after rendering the height no longer matters

upd: additionaly, for calculating multiline text height, we need col (cell) width cell for retreive line count

It seems that with the current v2 architecture (render method with provider and cell arguments) this is not possible

RuSPanzer avatar Sep 20 '23 20:09 RuSPanzer

Actually there is a method GetLinesQuantity(text string, fontFamily props.Text, colWidth float64) int on text that only uses the colWidth. Maybe the method should be GetComputedHeight(colWidth) float64. So we can use it.

About the GetLinesQuantity(text string, fontFamily props.Text, colWidth float64) int , maybe we could have a GetHeight(text string, fontFamily props.Text, colWidth float64) flaot64 to return the actually height.

johnfercher avatar Sep 20 '23 20:09 johnfercher

This issue is not an easy one haha

johnfercher avatar Sep 20 '23 20:09 johnfercher

GetLinesQuantity(text string, fontFamily props.Text, colWidth float64) int method possible only in gofpdfProvider, because written in gopdf implementation in internal.text package

but we the planned method GetHeight in pkg.components.text component, for calculating cell sizes before the rendering process, where provider not available yet.

If the correct height dimensions are not calculated before rendering, then it will be impossible to make a correct split on the page in the maroto.addRow method

but calculating the dimensions before rendering is impossible, because the rendering method must receive the already calculated cell :smile:

RuSPanzer avatar Sep 20 '23 21:09 RuSPanzer

Hmm, you are right. This is even harder than I was thinking. There are some easier ones that you could solve in this kanban of maroto (https://github.com/users/johnfercher/projects/1).

But if this is the one you want to solve... there is a method SetConfig() which is already used pre-rendering. Maybe we could change the rendering...the rendering would not receive more the cell. We would pass the cell in the SetConfig, and calculate the cell there. Storing in the structures... the rendering would only render the component using the pre-calculated values passed in the SetConfig

johnfercher avatar Sep 20 '23 21:09 johnfercher

It's a shame that this is not yet available. It is something that will impact you no matter what when you work with this library. It would be necessary to have a record of how many characters it supports for each column division (1 to 12), and depending on the length of the text to obtain the value of the row.

bubilla avatar Jul 18 '24 16:07 bubilla