From 584e7a57086a44a350a2a3d0f35a4764f45d4eed Mon Sep 17 00:00:00 2001 From: Aleksey Chernov Date: Fri, 9 Jul 2021 14:06:18 +0200 Subject: [PATCH 01/11] (Upstream) lvstring: fix signed integer overflow Fix lvstring.cpp:127:42: runtime error: signed integer overflow: 1775760480 * 31 cannot be represented in type 'int' --- crengine/src/lvstring.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crengine/src/lvstring.cpp b/crengine/src/lvstring.cpp index c52147a14..8e3ca78e4 100644 --- a/crengine/src/lvstring.cpp +++ b/crengine/src/lvstring.cpp @@ -64,7 +64,7 @@ static int size_8 = 0; /// get reference to atomic constant string for string literal e.g. cs8("abc") -- fast and memory effective const lString8 & cs8(const char * str) { - int index = (int)(((ptrdiff_t)str * CONST_STRING_BUFFER_HASH_MULT) & CONST_STRING_BUFFER_MASK); + unsigned int index = (unsigned int)(((ptrdiff_t)str * CONST_STRING_BUFFER_HASH_MULT) & CONST_STRING_BUFFER_MASK); for (;;) { const void * p = const_ptrs_8[index]; if (p == str) { @@ -93,7 +93,7 @@ static int size_32 = 0; /// get reference to atomic constant wide string for string literal e.g. cs32("abc") -- fast and memory effective const lString32 & cs32(const char * str) { - int index = (int)(((ptrdiff_t)str * CONST_STRING_BUFFER_HASH_MULT) & CONST_STRING_BUFFER_MASK); + unsigned int index = (unsigned int)(((ptrdiff_t)str * CONST_STRING_BUFFER_HASH_MULT) & CONST_STRING_BUFFER_MASK); for (;;) { const void * p = const_ptrs_32[index]; if (p == str) { @@ -118,7 +118,7 @@ const lString32 & cs32(const char * str) { /// get reference to atomic constant wide string for string literal e.g. cs32(U"abc") -- fast and memory effective const lString32 & cs32(const lChar32 * str) { - int index = (((int)((ptrdiff_t)str)) * CONST_STRING_BUFFER_HASH_MULT) & CONST_STRING_BUFFER_MASK; + unsigned int index = (((unsigned int)((ptrdiff_t)str)) * CONST_STRING_BUFFER_HASH_MULT) & CONST_STRING_BUFFER_MASK; for (;;) { const void * p = const_ptrs_32[index]; if (p == str) { From 67d8ed4a8bf28a9cd28afa0785605216c53ae5bd Mon Sep 17 00:00:00 2001 From: Aleksey Chernov Date: Fri, 9 Jul 2021 14:06:20 +0200 Subject: [PATCH 02/11] (Upstream) lvopc: fix memory leak Direct leak of 64 byte(s) in 4 object(s) allocated from: 00 in operator new(unsigned long) (libasan.so.6+0xb0087) 01 in OpcPart::readRelations() lvopc.cpp:73 02 in OpcPart::getRelatedPartName(char32_t const*, lString32) lvopc.cpp:33 03 in fb3ImportContext::openBook() fb3fmt.cpp:142 Indirect leak of 512 byte(s) in 4 object(s) allocated from: 00 in operator new[](unsigned long) (libasan.so.6+0xb01f7) 01 in LVHashTable::LVHashTable(int) lvhashtable.h:107 02 in OpcPart::readRelations() lvopc.cpp:73 03 in OpcPart::getRelatedPartName(char32_t const*, lString32) lvopc.cpp:33 04 in fb3ImportContext::openBook() fb3fmt.cpp:142 Indirect leak of 96 byte(s) in 4 object(s) allocated from: 00 in operator new(unsigned long) (libasan.so.6+0xb0087) 01 in LVHashTable::set(lString32 const&, lString32) vhashtable.h:174 02 in OpcPart::readRelations() lvopc.cpp:77 03 in OpcPart::getRelatedPartName(char32_t const*, lString32) lvopc.cpp:33 04 in fb3ImportContext::openBook() fb3fmt.cpp:142 --- crengine/src/lvopc.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/crengine/src/lvopc.cpp b/crengine/src/lvopc.cpp index 927c8049e..057602494 100644 --- a/crengine/src/lvopc.cpp +++ b/crengine/src/lvopc.cpp @@ -5,7 +5,13 @@ static const lChar32 * const OPC_PropertiesContentType = U"application/vnd.openx OpcPart::~OpcPart() { - m_relations.clear(); + LVHashTable *>::iterator it = m_relations.forwardIterator(); + LVHashTable *>::pair* p; + while ((p = it.next()) != NULL) { + LVHashTable* relationsTable = p->value; + if (relationsTable) + delete relationsTable; + } } LVStreamRef OpcPart::open() From 7d94624a886cb3233431df6611923c2e8b3d314d Mon Sep 17 00:00:00 2001 From: Aleksey Chernov Date: Fri, 9 Jul 2021 14:06:22 +0200 Subject: [PATCH 03/11] (Upstream) ldomDocument(): fix memory leak Direct leak of 64 byte(s) in 1 object(s) allocated from: 00 in operator new(unsigned long) (libasan.so.6+0xb0087) 01 in tinyNodeCollection::allocTinyElement(ldomNode*, unsigned short, unsigned short) lvtinydom.cpp:16430 02 in ldomDocument::ldomDocument() lvtinydom.cpp:4136 03 in LVDocView::createEmptyDocument() lvdocview.cpp:4797 04 in LVDocView::loadDocumentInt(LVFastRef, bool) lvdocview.cpp:4438 05 in LVDocView::LoadDocument(char32_t const*, bool) lvdocview.cpp:4127 --- crengine/src/lvtinydom.cpp | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/crengine/src/lvtinydom.cpp b/crengine/src/lvtinydom.cpp index 62e6e2a74..cecc29bcf 100644 --- a/crengine/src/lvtinydom.cpp +++ b/crengine/src/lvtinydom.cpp @@ -3896,14 +3896,8 @@ ldomDocument::ldomDocument() , lists(100) { _docIndex = ldomNode::registerDocument(this); - allocTinyElement(NULL, 0, 0); - // Note: valgrind reports (sometimes, when some document is opened or closed, - // with metadataOnly or not) a memory leak (64 bytes in 1 blocks are definitely - // lost), about this, created in allocTinyElement(): - // tinyElement * elem = new tinyElement(...) - // possibly because it's not anchored anywhere. - // Attempt at anchoring into a _nullNode, and calling ->detroy() - // in ~ldomDocument(), did not prevent this report, and caused other ones... + ldomNode* node = allocTinyElement(NULL, 0, 0); + node->persist(); //new ldomElement( this, NULL, 0, 0, 0 ); //assert( _instanceMapCount==2 ); From 77f43f399aabf85b17a7e5c96a0b8a1a0ebc201b Mon Sep 17 00:00:00 2001 From: Aleksey Chernov Date: Fri, 9 Jul 2021 14:06:24 +0200 Subject: [PATCH 04/11] (Upstream) lvtinydom: fix memory leaks Direct leak of 128 byte(s) in 2 object(s) allocated from: 0 in operator new(unsigned long) (libasan.so.6+0xb0087) 1 in ldomNode::modify() lvtinydom.cpp:19148 2 in ldomNode::removeChild(unsigned int) lvtinydom.cpp:18643 3 in ldomNode::removeChildren(int, int) lvtinydom.cpp:5845 4 in ldomNode::autoboxChildren(int, int, bool) lvtinydom.cpp:5995 5 in ldomNode::initNodeRendMethod() lvtinydom.cpp:6842 6 in ldomElementWriter::onBodyExit() lvtinydom.cpp:7815 7 in ldomElementWriter::~ldomElementWriter() lvtinydom.cpp:7955 8 in ldomDocumentWriterFilter::popUpTo(ldomElementWriter*, unsigned short, int) lvtinydom.cpp:13895 9 in ldomDocumentWriterFilter::AutoOpenClosePop(int, unsigned short) lvtinydom.cpp:14126 10 in ldomDocumentWriterFilter::OnTagClose(char32_t const*, char32_t const*, bool) lvtinydom.cpp:14657 11 in LVXMLParser::Parse() lvxmlparser.cpp:278 Direct leak of 64 byte(s) in 1 object(s) allocated from: 0 in operator new(unsigned long) (libasan.so.6+0xb0087) 1 in ldomNode::modify() lvtinydom.cpp:19148 2 in ldomNode::insertChildElement(unsigned int, unsigned short, unsigned short) lvtinydom.cpp:18534 3 in ldomNode::autoboxChildren(int, int, bool) lvtinydom.cpp:5974 4 in ldomNode::initNodeRendMethod() lvtinydom.cpp:6842 5 in ldomElementWriter::onBodyExit() lvtinydom.cpp:7815 6 in ldomElementWriter::~ldomElementWriter() lvtinydom.cpp:7955 7 in ldomDocumentWriterFilter::popUpTo(ldomElementWriter*, unsigned short, int) lvtinydom.cpp:13895 8 in ldomDocumentWriterFilter::AutoOpenClosePop(int, unsigned short) lvtinydom.cpp:14126 9 in ldomDocumentWriterFilter::OnTagClose(char32_t const*, char32_t const*, bool) lvtinydom.cpp:14657 Direct leak of 64 byte(s) in 1 object(s) allocated from: 0 in operator new(unsigned long) (libasan.so.6+0xb0087) 1 in tinyNodeCollection::allocTinyElement(ldomNode*, unsigned short, unsigned short) lvtinydom.cpp:16424 2 in ldomNode::insertChildElement(unsigned int, unsigned short, unsigned short) lvtinydom.cpp:18538 3 in ldomNode::autoboxChildren(int, int, bool) lvtinydom.cpp:5974 4 in ldomNode::initNodeRendMethod() lvtinydom.cpp:6842 5 in ldomElementWriter::onBodyExit() lvtinydom.cpp:7815 6 in ldomElementWriter::~ldomElementWriter() lvtinydom.cpp:7955 7 in ldomDocumentWriterFilter::popUpTo(ldomElementWriter*, unsigned short, int) lvtinydom.cpp:13895 8 in ldomDocumentWriterFilter::AutoOpenClosePop(int, unsigned short) lvtinydom.cpp:14126 9 in ldomDocumentWriterFilter::OnTagClose(char32_t const*, char32_t const*, bool) lvtinydom.cpp:14657 10 in LVXMLParser::Parse() lvxmlparser.cpp:278 11 in LVHTMLParser::Parse() lvhtmlparser.cpp:157 Direct leak of 144 byte(s) in 2 object(s) allocated from: 0 in operator new(unsigned long) (libasan.so.6+0xb0087) 1 in CCRTable::LookupElem(ldomNode*, int, int) (cr3qt/cr3+0x1f6681a) 2 in CCRTable::LookupElem(ldomNode*, int, int) (cr3qt/cr3+0x1f65cb3) 3 in CCRTable::CCRTable(ldomNode*, int, bool, int, int, bool, bool, int, bool) lvrend.cpp:2243 4 in renderTable(LVRendPageContext&, ldomNode*, int, int, int, bool, int, int&, int, bool, bool, bool) lvrend.cpp:2276 5 in renderBlockElementEnhanced(FlowState*, ldomNode*, int, int, unsigned int) lvrend.cpp:7639 6 in renderBlockElementEnhanced(FlowState*, ldomNode*, int, int, unsigned int) lvrend.cpp:7860 7 in renderBlockElement(LVRendPageContext&, ldomNode*, int, int, int, int, int, int, int*, unsigned int) lvrend.cpp:8393 8 in renderBlockElement(LVRendPageContext&, ldomNode*, int, int, int, int, int, int, int*) lvrend.cpp:8409 9 in LVFormatter::measureText() lvtextfm.cpp:2045 10 in LVFormatter::processParagraph(int, int, bool) lvtextfm.cpp:3809 11 in LVFormatter::splitParagraphs() lvtextfm.cpp:4461 12 in LVFormatter::format() lvtextfm.cpp:4508 13 in LFormattedText::Format(unsigned short, unsigned short, int, int, int, bool, BlockFloatFootprint*) lvtextfm.cpp:4612 14 in ldomNode::renderFinalBlock(LVRef&, RenderRectAccessor*, int, BlockFloatFootprint*) lvtinydom.cpp:19074 15 in renderBlockElementEnhanced(FlowState*, ldomNode*, int, int, unsigned int) lvrend.cpp:8167 Indirect leak of 896 byte(s) in 28 object(s) allocated from: 0 in __interceptor_realloc (libasan.so.6+0xaea88) 1 in lxmlAttribute* cr_realloc(lxmlAttribute*, unsigned long) (cr3qt/cr3+0x1c890d6) 2 in ldomAttributeCollection::set(unsigned short, unsigned short, unsigned int) lvtinydom.cpp:3933 3 in ldomNode::setAttributeValue(unsigned short, unsigned short, char32_t const*) lvtinydom.cpp:17001 4 in fixupMathML mathml.cpp:2157 5 in fixupMathMLRecursive mathml.cpp:1737 6 in fixupMathMLRecursive mathml.cpp:1734 14 in fixupMathMLRecursive mathml.cpp:1734 15 in fixupMathMLMathElement(ldomNode*) mathml.cpp:1708 16 in ldomNode::initNodeRendMethod() lvtinydom.cpp:7777 17 in ldomElementWriter::onBodyExit() lvtinydom.cpp:7819 18 in ldomElementWriter::~ldomElementWriter() lvtinydom.cpp:7959 --- crengine/src/lvtinydom.cpp | 4 ++++ crengine/src/mathml.cpp | 1 + crengine/src/mathml_table_ext.h | 2 +- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/crengine/src/lvtinydom.cpp b/crengine/src/lvtinydom.cpp index cecc29bcf..42327f759 100644 --- a/crengine/src/lvtinydom.cpp +++ b/crengine/src/lvtinydom.cpp @@ -6170,6 +6170,7 @@ int initTableRendMethods( ldomNode * enode, int state ) first_unproper = -1; last_unproper = -1; } + child->persist(); } // if ( state==0 ) { // dumpRendMethods( enode, cs32(" ") ); @@ -7617,6 +7618,8 @@ void ldomNode::initNodeRendMethod() } } #endif + + persist(); } #endif @@ -18623,6 +18626,7 @@ void ldomNode::moveItemsTo( ldomNode * destination, int startChildIndex, int end item->setParentNode(destination); destination->addChild( item->getDataIndex() ); } + destination->persist(); // TODO: renumber rest of children in necessary /*#ifdef _DEBUG if ( !_document->checkConsistency( false ) ) diff --git a/crengine/src/mathml.cpp b/crengine/src/mathml.cpp index b84389182..ff852944d 100644 --- a/crengine/src/mathml.cpp +++ b/crengine/src/mathml.cpp @@ -1733,6 +1733,7 @@ static void fixupMathMLRecursive( ldomNode * node, bool is_in_script ) { } } fixupMathML( node, is_in_script ); + node->persist(); return; // If we would be inseting nodes in the upper tree, we might // want to use a non-recursive subtree walker diff --git a/crengine/src/mathml_table_ext.h b/crengine/src/mathml_table_ext.h index 3da88a775..e38cf5422 100644 --- a/crengine/src/mathml_table_ext.h +++ b/crengine/src/mathml_table_ext.h @@ -75,7 +75,7 @@ void CCRTable::MathML_checkAndTweakTableElement() { i++; // keep it in row1 as a filler for the missing cell at top right } else { - row1->cells.remove(i); // remove it + delete row1->cells.remove(i); // remove it } insert_at = 0; // next cells will be moved at start of rows next_is_sup = false; // next is a sub From 0c1491c4a336e420b96f8758db50e4218069cff8 Mon Sep 17 00:00:00 2001 From: poire-z Date: Fri, 9 Jul 2021 14:06:26 +0200 Subject: [PATCH 05/11] LVArray: fix indexOf() to work with any type --- crengine/include/lvarray.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crengine/include/lvarray.h b/crengine/include/lvarray.h index 6560a4857..12e6d8a5f 100644 --- a/crengine/include/lvarray.h +++ b/crengine/include/lvarray.h @@ -226,10 +226,10 @@ class LVArray _count++; } - /// returns index of specified value, -1 if not found - int indexOf(int value) const { + /// returns index of specified item, -1 if not found + int indexOf(T item) const { for ( int i=0; i<_count; i++ ) { - if ( _array[i] == value ) + if ( _array[i] == item ) return i; } return -1; From 157515983748a67df1e0c79f60cdb5fd39bf31a5 Mon Sep 17 00:00:00 2001 From: poire-z Date: Fri, 9 Jul 2021 14:06:28 +0200 Subject: [PATCH 06/11] LVDocView: fix m_twoVisiblePagesAsOnePageNumber uninitialised --- crengine/src/lvdocview.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crengine/src/lvdocview.cpp b/crengine/src/lvdocview.cpp index 0030f438e..19ee19fb8 100644 --- a/crengine/src/lvdocview.cpp +++ b/crengine/src/lvdocview.cpp @@ -173,7 +173,7 @@ LVDocView::LVDocView(int bitsPerPixel, bool noDefaultDocument) : m_pageMargins(DEFAULT_PAGE_MARGIN, DEFAULT_PAGE_MARGIN / 2 /*+ INFO_FONT_SIZE + 4 */, DEFAULT_PAGE_MARGIN, DEFAULT_PAGE_MARGIN / 2), - m_pagesVisible(2), m_pagesVisible_onlyIfSane(true), + m_pagesVisible(2), m_pagesVisible_onlyIfSane(true), m_twoVisiblePagesAsOnePageNumber(false), m_pageHeaderInfo(PGHDR_PAGE_NUMBER #ifndef LBOOK | PGHDR_CLOCK From 348b8ff7229248bd6758fc89097951bda0d5d4b7 Mon Sep 17 00:00:00 2001 From: poire-z Date: Fri, 9 Jul 2021 14:06:30 +0200 Subject: [PATCH 07/11] CSS: line-break: accept "-cr-loose" to ignore no-break-spaces --- crengine/include/cssdef.h | 3 ++- crengine/src/lvstsheet.cpp | 1 + crengine/src/textlang.cpp | 8 ++++++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/crengine/include/cssdef.h b/crengine/include/cssdef.h index 4239c185c..970ec1080 100644 --- a/crengine/include/cssdef.h +++ b/crengine/include/cssdef.h @@ -348,7 +348,8 @@ enum css_line_break_t { css_lb_normal, css_lb_loose, css_lb_strict, - css_lb_anywhere + css_lb_anywhere, + css_lb_cr_loose // private value "line-break: -cr-loose" to ignore   }; /// word-break property values diff --git a/crengine/src/lvstsheet.cpp b/crengine/src/lvstsheet.cpp index 3a1623bfb..8c4fffc7e 100644 --- a/crengine/src/lvstsheet.cpp +++ b/crengine/src/lvstsheet.cpp @@ -1864,6 +1864,7 @@ static const char * css_lb_names[] = "loose", "strict", "anywhere", + "-cr-loose", NULL }; diff --git a/crengine/src/textlang.cpp b/crengine/src/textlang.cpp index 30eeb9860..eb839a8ca 100644 --- a/crengine/src/textlang.cpp +++ b/crengine/src/textlang.cpp @@ -771,6 +771,14 @@ lChar32 TextLangCfg::getCssLbCharSub(css_line_break_t css_linebreak, css_word_br } #endif } + + // Private keyword: "line-break: -cr-loose", to ignore no-break-space (some books + // may abuse   between words like article and noun, possibly to make it easier + // for people with reading difficulties) + if ( css_linebreak == css_lb_cr_loose ) { + if ( ch==0x00A0 ) return ' '; // non-breaking space => space + if ( ch==0x2011 ) return 0x2010; // non-breaking hyphen => hyphen + } } return ch; } From 1e1b6e13aef01ffae4728bf15f6ad2a489609a25 Mon Sep 17 00:00:00 2001 From: poire-z Date: Fri, 9 Jul 2021 14:06:32 +0200 Subject: [PATCH 08/11] CSS: font-variant: extend effect of "normal" and "none" --- crengine/src/lvrend.cpp | 10 ++++++++-- crengine/src/lvstsheet.cpp | 11 ++++++++++- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/crengine/src/lvrend.cpp b/crengine/src/lvrend.cpp index 7bb13ef34..3df3621fe 100644 --- a/crengine/src/lvrend.cpp +++ b/crengine/src/lvrend.cpp @@ -10078,8 +10078,14 @@ void setNodeStyle( ldomNode * enode, css_style_ref_t parent_style, LVFontRef par // it's better or not: we'll see. // (Note that we can use * { font-variant: normal !important; } to // stop any font-variant without !important from being applied.) - pstyle->font_features.value |= parent_style->font_features.value; - pstyle->font_features.type = css_val_unspecified; + // There is one case where we don't inherit: when styles had this + // node ending up being (css_val_unspecified, 0), which can only + // happen with "font-variant(-*): normal/none", that might be + // used to prevent some upper font-variant to be inherited. + if ( pstyle->font_features.type == css_val_inherited || pstyle->font_features.value != 0 ) { + pstyle->font_features.value |= parent_style->font_features.value; + pstyle->font_features.type = css_val_unspecified; + } // cr_hint is also a bitmap, and only some bits are inherited. // A node starts with (css_val_inherited, 0), but if some diff --git a/crengine/src/lvstsheet.cpp b/crengine/src/lvstsheet.cpp index 8c4fffc7e..3ae95952c 100644 --- a/crengine/src/lvstsheet.cpp +++ b/crengine/src/lvstsheet.cpp @@ -3186,7 +3186,16 @@ void LVCssDeclaration::apply( css_style_rec_t * style ) case cssd_font_features: // We want to 'OR' the bitmap from any declaration that is to be applied to this node // (while still ensuring !important). - style->ApplyAsBitmapOr( read_length(p), &style->font_features, imp_bit_font_features, is_important ); + { + css_length_t font_features = read_length(p); + if ( font_features.value == 0 && font_features.type == css_val_unspecified ) { + // except if "font-variant: normal/none", which resets all previously set bits + style->Apply( font_features, &style->font_features, imp_bit_font_features, is_important ); + } + else { + style->ApplyAsBitmapOr( font_features, &style->font_features, imp_bit_font_features, is_important ); + } + } break; case cssd_text_indent: style->Apply( read_length(p), &style->text_indent, imp_bit_text_indent, is_important ); From 9c8fccc985a3cf99f1d65ec66b5c9e685273edf5 Mon Sep 17 00:00:00 2001 From: poire-z Date: Fri, 9 Jul 2021 14:06:35 +0200 Subject: [PATCH 09/11] lvrend: fix BlockFloat footnotes context line associations Footnote links inside floats were previously associated to the line before the float, which could make them shown on the previous page if the float needs a page break to be shown without break. We now delay these links associations, and associate them only when the float is fully passed by, so the footnote is never before the float. --- crengine/src/lvrend.cpp | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/crengine/src/lvrend.cpp b/crengine/src/lvrend.cpp index 3df3621fe..2e69b4b03 100644 --- a/crengine/src/lvrend.cpp +++ b/crengine/src/lvrend.cpp @@ -5099,10 +5099,11 @@ class FlowState { // allows knowing how much the main text glyphs and hanging punctuation // can protrude inside this float (we limit that to the first level margin, // not including any additional inner padding or margin) + lString32Collection links; // footnote links found in this float bool is_right; bool final_pos; // true if y0/y1 are the final absolute position and this // float should not be moved when pushing vertical margins. - BlockFloat( int x0, int y0, int x1, int y1, bool r, int l, bool f, ldomNode * n=NULL) : + BlockFloat( int x0, int y0, int x1, int y1, bool r, int l, bool f, ldomNode * n=NULL, lString32Collection * linkids=NULL) : lvRect(x0,y0,x1,y1), level(l), inward_margin(0), @@ -5120,7 +5121,19 @@ class FlowState { else inward_margin = (x1 - x0) - (fmt.getX() + fmt.getWidth()); } + if (linkids && linkids->length() > 0) { + // This floats has footnotes, that we'll push to page context + // when the float is passed by + links.addAll(*linkids); + } } + void addLinks(LVRendPageContext * context) { + // Associate footnote links with the last line added to context + for ( int n=0; naddLink( links[n] ); + } + links.clear(); // Be sure we don't add them again + } }; int direction; // flow inline direction (LTR/RTL) lInt32 lang_node_idx; // dataIndex of nearest upper node with a lang="" attribute (0 if none) @@ -5215,6 +5228,7 @@ class FlowState { // by leaveBlockLevel(). But let's ensure we clean up well. for (int i=_floats.length()-1; i>=0; i--) { BlockFloat * flt = _floats[i]; + flt->addLinks(&context); _floats.remove(i); delete flt; } @@ -6123,6 +6137,7 @@ class FlowState { for (int i=_floats.length()-1; i>=0; i--) { BlockFloat * flt = _floats[i]; if (flt->level > level) { + flt->addLinks(&context); _floats.remove(i); delete flt; } @@ -6171,6 +6186,7 @@ class FlowState { if (flt->bottom <= c_y) { // This float is past, we shouldn't have to worry // about it anymore + flt->addLinks(&context); _floats.remove(i); delete flt; } @@ -6322,7 +6338,7 @@ class FlowState { } return fit_top_y; } - void addFloat( ldomNode * node, css_clear_t clear, bool is_right, int top_margin ) { + void addFloat( ldomNode * node, css_clear_t clear, bool is_right, int top_margin, lString32Collection * link_ids=NULL ) { RenderRectAccessor fmt( node ); int width = fmt.getWidth(); int height = fmt.getHeight(); @@ -6339,7 +6355,7 @@ class FlowState { fy = pos_y; int fx = 0; fy = getYWithAvailableWidth(fy, width, height, x_min, x_max, fx, is_right); - _floats.push( new BlockFloat( fx, fy, fx + width, fy + height, is_right, level, true, node) ); + _floats.push( new BlockFloat( fx, fy, fx + width, fy + height, is_right, level, true, node, link_ids) ); // Get relative coordinates to current container top shift_x = fx - x_min; @@ -7830,14 +7846,9 @@ void renderBlockElementEnhanced( FlowState * flow, ldomNode * enode, int x, int // no border (if borders, only the padding can be used). renderBlockElement( alt_context, child, (is_rtl ? 0 : list_marker_padding) + padding_left, 0, width - list_marker_padding - padding_left - padding_right, 0, 0, direction ); - flow->addFloat(child, child_clear, is_right, flt_vertical_margin); - // Gather footnotes links accumulated by alt_context - lString32Collection * link_ids = alt_context.getLinkIds(); - if (link_ids->length() > 0) { - for ( int n=0; nlength(); n++ ) { - flow->getPageContext()->addLink( link_ids->at(n) ); - } - } + flow->addFloat(child, child_clear, is_right, flt_vertical_margin, alt_context.getLinkIds()); + // We pass the footnote links accumulated by alt_context, + // so they can be forwarded onto the main context lines. } else { css_clear_t child_clear = child_style->clear; From 4ad486bd79f7aecc35abcf89dd63888c79e5cd56 Mon Sep 17 00:00:00 2001 From: poire-z Date: Fri, 9 Jul 2021 14:06:37 +0200 Subject: [PATCH 10/11] In-page footnotes: support links inside inline-block --- crengine/include/lvtextfm.h | 3 ++ crengine/src/lvrend.cpp | 11 ++++++ crengine/src/lvtextfm.cpp | 74 +++++++++++++++++++++++++++++-------- 3 files changed, 72 insertions(+), 16 deletions(-) diff --git a/crengine/include/lvtextfm.h b/crengine/include/lvtextfm.h index 3b79102c9..5dfec1fe9 100644 --- a/crengine/include/lvtextfm.h +++ b/crengine/include/lvtextfm.h @@ -266,6 +266,7 @@ typedef struct lUInt32 height; /**< height of text fragment */ lUInt16 width; /**< width of text fragment */ lUInt16 page_height; /**< max page height */ + LVHashTable * inlineboxes_links; // Each line box starts with a zero-width inline box (called "strut") with // the element's font and line height properties: @@ -482,6 +483,8 @@ class LFormattedText return m_pbuffer->floats[index]; } + lString32Collection * GetInlineBoxLinks( ldomNode * node ); + void Draw( LVDrawBuf * buf, int x, int y, ldomMarkedRangeList * marks = NULL, ldomMarkedRangeList *bookmarks = NULL ); bool isReusable() { return m_pbuffer->is_reusable; } diff --git a/crengine/src/lvrend.cpp b/crengine/src/lvrend.cpp index 2e69b4b03..4d8bb171a 100644 --- a/crengine/src/lvrend.cpp +++ b/crengine/src/lvrend.cpp @@ -8289,6 +8289,17 @@ void renderBlockElementEnhanced( FlowState * flow, ldomNode * enode, int x, int // check link start flag for every word if ( line->words[w].flags & LTEXT_WORD_IS_LINK_START ) { const src_text_fragment_t * src = txform->GetSrcInfo( line->words[w].src_text_index ); + if ( line->words[w].flags & LTEXT_WORD_IS_INLINE_BOX ) { + // With an inline box, links were already parsed when it was rendered, + // and have been stored in the txform buffer + lString32Collection * links = txform->GetInlineBoxLinks( (ldomNode*)src->object ); + if ( links ) { + for ( int n=0; nlength(); n++ ) { + flow->getPageContext()->addLink( links->at(n), link_insert_pos ); + } + } + continue; + } if ( src && src->object ) { ldomNode * node = (ldomNode*)src->object; ldomNode * parent = node->getParentNode(); diff --git a/crengine/src/lvtextfm.cpp b/crengine/src/lvtextfm.cpp index 9ecb054b0..f3d97e787 100644 --- a/crengine/src/lvtextfm.cpp +++ b/crengine/src/lvtextfm.cpp @@ -197,6 +197,15 @@ void lvtextFreeFormatter( formatted_text_fragment_t * pbuffer ) } free( pbuffer->floats ); } + if (pbuffer->inlineboxes_links) + { + LVHashTable::iterator it = pbuffer->inlineboxes_links->forwardIterator(); + LVHashTable::pair* pair; + while ( (pair = it.next()) ) { + delete pair->value; + } + free( pbuffer->inlineboxes_links ); + } free(pbuffer); } @@ -2051,22 +2060,22 @@ class LVFormatter { RENDER_RECT_SET_FLAG(fmt, BOX_IS_RENDERED); // We'll have alignLine() do the fmt.setX/Y once it is fully positioned - // We'd like to gather footnote links accumulated by alt_context - // (we do that for floats), but it's quite more complicated: - // we have them here too early, and we would need to associate - // the links to this "char" index, so needing in LVFormatter - // something like: - // LVHashTable m_inlinebox_links - // When adding this inlineBox to a frmline, we could then get back - // the links, and associate them to the frmline (so, needing a - // new field holding a lString32Collection, which would hold - // all the links in all the inlineBoxs part of that line). - // Finally, in renderBlockElementEnhanced, when adding - // links for words, we'd also need to add the one found - // in the frmline's lString32Collection. - // A bit complicated, for a probably very rare case, so - // let's just forget it and not have footnotes from inlineBox - // among our in-page footnotes... + // Gather footnote links accumulated by alt_context + lString32Collection * link_ids = alt_context.getLinkIds(); + if (link_ids->length() > 0) { + if ( m_pbuffer->inlineboxes_links == NULL ) { + m_pbuffer->inlineboxes_links = new LVHashTable(16); + } + lString32Collection * links; + lUInt32 key = node->getDataIndex(); + if ( !m_pbuffer->inlineboxes_links->get(key, links) ) { + links = new lString32Collection(); + m_pbuffer->inlineboxes_links->set(key, links); + } + for ( int n=0; nlength(); n++ ) { + links->add( link_ids->at(n) ); + } + } } // (renderBlockElement() above may update our RenderRectAccessor(), // so (re)get it only now) @@ -2990,6 +2999,17 @@ class LVFormatter { baseline_to_bottom = word->o.height - word->o.baseline; // We can't really ensure strut_confined with inline-block boxes, // or we could miss content (it would be overwritten by next lines) + if ( m_pbuffer->inlineboxes_links ) { + // The buffer has some inline boxes with footnote links. + // If this inline box has some, let lvrend.cpp know, so it can + // fetch them when adding this line to the page split context + lString32Collection * links; + lUInt32 key = ((ldomNode *) srcline->object)->getDataIndex(); + if ( m_pbuffer->inlineboxes_links->get(key, links) ) { + word->flags |= LTEXT_WORD_IS_LINK_START; + // we re-use this flag already used by lvrend.cpp + } + } } else { // image word->flags = LTEXT_WORD_IS_OBJECT; @@ -4536,6 +4556,18 @@ static void freeFrmLines( formatted_text_fragment_t * m_pbuffer ) } m_pbuffer->floats = NULL; m_pbuffer->floatcount = 0; + + // Also clear inlinebox links containers + if (m_pbuffer->inlineboxes_links) + { + LVHashTable::iterator it = m_pbuffer->inlineboxes_links->forwardIterator(); + LVHashTable::pair* pair; + while ( (pair = it.next()) ) { + delete pair->value; + } + free( m_pbuffer->inlineboxes_links ); + } + m_pbuffer->inlineboxes_links = NULL; } // experimental formatter @@ -4617,6 +4649,16 @@ lUInt32 LFormattedText::Format(lUInt16 width, lUInt16 page_height, int para_dire return h; } +lString32Collection * LFormattedText::GetInlineBoxLinks( ldomNode * node ) { + if ( m_pbuffer->inlineboxes_links ) { + lString32Collection * links; + if ( m_pbuffer->inlineboxes_links->get(node->getDataIndex(), links) ) { + return links; + } + } + return NULL; +} + void LFormattedText::setImageScalingOptions( img_scaling_options_t * options ) { m_pbuffer->img_zoom_in_mode_block = options->zoom_in_block.mode; From d377ed8b05d9fda353a12c7355377cc12f331deb Mon Sep 17 00:00:00 2001 From: poire-z Date: Fri, 9 Jul 2021 14:06:39 +0200 Subject: [PATCH 11/11] Page splitting: revamp, fix some issues Rewritten page splitter, which should be easier to understand and tweak. The old code was quite tedious, trying to do everything in one single pass. This new code first finds out group of consecutive lines that should be kept together (break avoid), and process each group when they end, and only then the footnotes they contain (with delay if they don't fit). This fixes 2 small issues: - footnotes could slice a paragraph with floats on the line the footnotes happen; now, they are delayed till the end of the block, and the full block can go to the next page (with its footnotes) if it is taller than the current page. - if a first footnote and its separator didn't fit on a page, a new page was made so it can be added, leaving some blank at bottom of previous page ; now, we delay footnotes in this case, allowing an additional line of the main text to be added if it fits. --- crengine/src/lvpagesplitter.cpp | 464 +++++++++++++++++++++++++++++++- crengine/src/lvtinydom.cpp | 2 +- 2 files changed, 462 insertions(+), 4 deletions(-) diff --git a/crengine/src/lvpagesplitter.cpp b/crengine/src/lvpagesplitter.cpp index 95ab8f09f..2271c2bb5 100644 --- a/crengine/src/lvpagesplitter.cpp +++ b/crengine/src/lvpagesplitter.cpp @@ -14,7 +14,10 @@ #include "../include/lvtinydom.h" #include -// Uncomment for debugging page splitting algorithm: +// Uncomment to use old page splitter code +// #define USE_LEGACY_PAGESPLITTER + +// Uncomment for debugging legacy page splitting algorithm: // #define DEBUG_PAGESPLIT // (Also change '#if 0' around 'For debugging lvpagesplitter.cpp' // to '#if 1' in src/lvdocview.cpp to get a summary of pages. @@ -172,6 +175,7 @@ void LVRendPageContext::AddLine( int starty, int endy, int flags ) // between text and first footnote) #define FOOTNOTE_MARGIN_REM 1 +#ifdef USE_LEGACY_PAGESPLITTER // helper class struct PageSplitState { public: @@ -769,18 +773,471 @@ struct PageSplitState { return false; } }; +#endif // USE_LEGACY_PAGESPLITTER + +#ifndef USE_LEGACY_PAGESPLITTER +struct PageSplitState2 { +public: + LVRendPageList * page_list; + LVPtrVector & lines; + int page_height; + int doc_font_size; + int footnote_margin; + int nb_lines; + + int cur_page_top; + int cur_page_bottom; + int cur_page_footnotes_h; + int cur_page_nb_lines; + int cur_page_nb_lines_rtl; + int cur_page_nb_footnotes_lines; + int cur_page_nb_footnotes_lines_rtl; + int cur_page_flow; + int prev_page_flow; + + LVArray cur_page_footnotes; + LVArray cur_page_seen_footnotes; // footnotes already on this page, to avoid duplicates + LVArray delayed_footnotes; // footnotes to be displayed on a next page + + PageSplitState2(LVRendPageList * pl, LVPtrVector & ls, int pageHeight, int docFontSize) + : page_list(pl) // output + , lines(ls) // input + , page_height(pageHeight) // parameters + , doc_font_size(docFontSize) + { + footnote_margin = FOOTNOTE_MARGIN_REM * doc_font_size; + nb_lines = lines.length(); + prev_page_flow = 0; + // With FB2 books with a cover, a first page has already been added + // by ldomDocument::render(), and the first content line's y is not 0 + resetCurPageData(nb_lines ? lines[0]->getStart() : 0); + } + + void resetCurPageData(int page_top) { + cur_page_top = page_top; + cur_page_bottom = page_top; + cur_page_footnotes_h = 0; + cur_page_nb_lines = 0; + cur_page_nb_lines_rtl = 0; + cur_page_nb_footnotes_lines = 0; + cur_page_nb_footnotes_lines_rtl = 0; + cur_page_flow = -1; + cur_page_seen_footnotes.reset(); + } + + inline void accountLine(bool is_rtl=false) { + cur_page_nb_lines++; + if ( is_rtl) + cur_page_nb_lines_rtl++; + } + + inline void accountFootnoteLine(bool is_rtl=false) { + cur_page_nb_footnotes_lines++; + if ( is_rtl) + cur_page_nb_footnotes_lines_rtl++; + } + + inline int getCurPageMaxBottom() { + return cur_page_top + (page_height - cur_page_footnotes_h); + } + + inline int getAvailableHeightForFootnotes() { + // If no footnote height yet, account for the margin that would be used with the first line + return page_height - (cur_page_bottom - cur_page_top) + - (cur_page_footnotes_h > 0 ? cur_page_footnotes_h : footnote_margin); + } + + void flushCurrentPage(bool push_delayed=true) { + if ( cur_page_nb_lines > 0 || cur_page_nb_footnotes_lines > 0 ) { + #ifdef DEBUG_PAGESPLIT + printf("PS: ========= ADDING PAGE %d: %d > %d h=%d", + page_list->length(), cur_page_top, cur_page_bottom, cur_page_bottom - cur_page_top); + if ( cur_page_footnotes.length() > 0 ) + printf(" (+ %d footnotes, fh=%d)", cur_page_footnotes.length(), cur_page_footnotes_h); + printf(" [rtl l:%d/%d fl:%d/%d]\n", cur_page_nb_lines_rtl, cur_page_nb_lines, + cur_page_nb_footnotes_lines_rtl, cur_page_nb_footnotes_lines); + #endif + // Some content was added: create and add the new page + LVRendPageInfo * page = new LVRendPageInfo(cur_page_top, cur_page_bottom - cur_page_top, page_list->length()); + page_list->add(page); + + // Flag the page if it looks like it has more RTL than LTR content + if ( cur_page_nb_lines_rtl > cur_page_nb_lines / 2 ) + page->flags |= RN_PAGE_MOSTLY_RTL; + if ( cur_page_nb_footnotes_lines_rtl > cur_page_nb_footnotes_lines / 2 ) + page->flags |= RN_PAGE_FOOTNOTES_MOSTLY_RTL; + + // If no cur_page_flow set (page with only footnotes), use the one from previous page + page->flow = cur_page_flow < 0 ? prev_page_flow : cur_page_flow; + prev_page_flow = page->flow; + + // Add footnotes + if ( cur_page_footnotes.length() > 0 ) { + page->footnotes.add( cur_page_footnotes ); + cur_page_footnotes.reset(); + } + + // Make the new page start when the previous page ended + resetCurPageData(cur_page_bottom); + } + if ( push_delayed ) { + if ( !delayed_footnotes.empty() ) { + // Pushed delayed footnote on the new page + for ( int i=0; iflags & RN_SPLIT_DISCARD_AT_START) ) { + // Margin lines with SPLIT_AUTO are flagged with RN_SPLIT_DISCARD_AT_START: + // They should be shown inside a page, but should be discarded if they + // would be at the top of a new page. + start_if_new_page = i; + } + if ( lines_max_bottom < lines[i]->getEnd() ) { + lines_max_bottom = lines[i]->getEnd(); + } + if ( !has_footnotes && lines[i]->getLinks() ) { + has_footnotes = true; + } + } + #ifdef DEBUG_PAGESPLIT + printf("PS: adding (%d+%d) to current page %d>%d\n", lines[start]->getStart() - cur_page_bottom, + lines_max_bottom - lines[start]->getStart(), cur_page_top, cur_page_bottom); + #endif + // printf("addLinesToPage %d (%d %d) [%d->%d %d]\n", end-start+1, start, end, + // lines[start]->getStart(), lines[end]->getEnd(), lines[end]->getEnd() - lines[start]->getStart()); + if ( lines_max_bottom > getCurPageMaxBottom() ) { + // Does not fit on this page + if ( lines_max_bottom - cur_page_bottom <= page_height ) { + // If we end current page at the current cur_page_bottom, + // these lines will fit in a new empty page + flushCurrentPage(false); + // We don't push delayed footnotes (as this would reduce + // the available height and these lines may then not fit), + // so make sure that if there is any, we'll push them below + // before adding our lines' own footnotes + push_delayed_footnotes = true; + } + } + if ( cur_page_nb_lines == 0 && start_if_new_page != start ) { + // Empty page, either after our flush or already empty: + // ignore first line(s) marked with RN_SPLIT_DISCARD_AT_START + if ( start_if_new_page < 0 ) { + // Only discardable lines: ignore them, make the new page start after them + cur_page_top = lines[end]->getEnd(); + cur_page_bottom = cur_page_top; + return; + } + start = start_if_new_page; + cur_page_top = lines[start]->getStart(); + cur_page_bottom = cur_page_top; + } + // Either we have enough room to fit them all on the page, + // or we made a new page (but we may still not have enough room) + for ( int i=start; i <= end; i++ ) { + LVRendLineInfo * line = lines[i]; + if ( cur_page_flow < 0 ) { + // We set the flow from the first line (non-footnote) that will + // go on this page (we can't just use the last, as the last line + // in a non-linear flow may be some bottom vertical margin which + // will collapse with some of the next non non-linear one, and + // could get flow=0. (This was witnessed, not sure it's not a + // bug in lvrend.cpp non-linear-flow/sequence handling.) + cur_page_flow = line->flow; + } + int line_bottom = line->getEnd(); + if ( line_bottom <= getCurPageMaxBottom() ) { + // This line fits on the current page: just add it + if ( cur_page_bottom < line_bottom ) { + cur_page_bottom = line_bottom; + } + accountLine( line->flags & RN_LINE_IS_RTL); + } + else { + // This line does not fit on the current page. + // We could do what we did for the first line above: make + // a new page if this line would fit on an empty page, but + // this could add lots of blank space and kill the notion + // of break:avoid associated with all the lines we get here. + // It feels better (although possibly uglier) to keep them + // stacked onto each other and filling the pages, by slicing + // individual lines + // But if the line fits on a page, and the blank space we'd + // get at bottom by flushing the page is sufficiently small, + // we may do that. (If the line does not fit on a page, it + // will be sliced anyway, so we may as well slice it now.) + // Let's use 2em as "sufficiently small", which should ensure + // that lines of main content text (and small headings), that + // can't fit in what's left on a page, are pushed uncut to + // the next page, avoiding cutted lines of text. + if ( (line->getHeight() <= page_height) && + (getCurPageMaxBottom()-cur_page_bottom < doc_font_size*2) ) { + flushCurrentPage(false); + push_delayed_footnotes = true; // as done above + cur_page_flow = line->flow; + if ( line->flags & RN_SPLIT_DISCARD_AT_START ) { + // If this line we're slicing should be discarded + // at start, forget it now + cur_page_top = line_bottom; + cur_page_bottom = cur_page_top; + } + else { + cur_page_bottom = line_bottom; + accountLine( line->flags & RN_LINE_IS_RTL); + } + } + else { + // Slice this line. + // We have no knowledge of what this 'line' contains (text, + // image...), so we can't really find a better 'y' to cut + // at than the page height. We may then cut in the middle + // of a text line, and have halves displayed on different + // pages (although it seems crengine deals with displaying + // the cut line fully at start of next page). + // So, slice it and make pages as long as it keeps not fitting + bool discarded = false; + while ( line_bottom > getCurPageMaxBottom() ) { + cur_page_bottom = getCurPageMaxBottom(); + accountLine( line->flags & RN_LINE_IS_RTL); + // Don't push any delayed footnote, so get the most + // of space for these lines + flushCurrentPage(false); + push_delayed_footnotes = true; // as done above + if ( line->flags & RN_SPLIT_DISCARD_AT_START ) { + // If this line we're slicing should be discarded + // at start, forget it now + cur_page_top = line_bottom; + cur_page_bottom = cur_page_top; + discarded = true; + break; + } + cur_page_flow = line->flow; + } + if ( !discarded ) { + cur_page_bottom = line_bottom; + accountLine( line->flags & RN_LINE_IS_RTL); + } + } + } + } + // Handle footnotes (if any) after we have handled all the non-splittable main lines + if ( push_delayed_footnotes && !delayed_footnotes.empty() ) { + // Above, we flushed one or more new pages without pushing + // delayed footnotes, so push them now. + // But only if the first line of them fits. Otherwise, keep + // them delayed (our own footnotes will then be delayed too). + if ( delayed_footnotes[0]->getLines()[0]->getHeight() <= getAvailableHeightForFootnotes() ) { + for ( int i=0; iempty() ) + return; + // Avoid duplicated footnotes in the same page + if ( cur_page_seen_footnotes.indexOf(note) >= 0 ) + return; + cur_page_seen_footnotes.add(note); + + int note_nb_lines = note->getLines().length(); + int note_top = -1; + int note_bottom = -1; + for ( int i=0; i < note_nb_lines; i++ ) { + // Note: we don't ensure SPLIT_AVOID/ALWAYS inside footnotes + LVRendLineInfo * line = note->getLines()[i]; + if ( note_top < 0 ) + note_top = line->getStart(); + int new_note_bottom = line->getEnd(); + if ( new_note_bottom - note_top > getAvailableHeightForFootnotes() ) { + // This new line makes the footnote not fit + if ( note_bottom >= 0 ) { + // Some previous lines fitted: add them to current page + int note_height = note_bottom - note_top; + cur_page_footnotes.add( LVPageFootNoteInfo( note_top, note_height ) ); + if (cur_page_footnotes_h == 0) + cur_page_footnotes_h = footnote_margin; + cur_page_footnotes_h += note_height; + // We'll add a new footnote with remaining lines to the new page + note_top = line->getStart(); + } + flushCurrentPage(false); // avoid re-adding delayed footnotes, which we might be doing + // We're not yet done adding this footnote (so it will continue + // on the new page): consider it already present in this new page + // (even if one won't see the starting text and footnote number, + // it's better than seeing again the same duplicated footnote text) + cur_page_seen_footnotes.add(note); + } + // This footnote line fits + note_bottom = new_note_bottom; + accountFootnoteLine(line->flags & RN_LINE_IS_RTL); + } + // Add this footnote (or the remaining part of it) to current page + int note_height = note_bottom - note_top; + if ( note_height > 0 ) { + cur_page_footnotes.add( LVPageFootNoteInfo( note_top, note_height ) ); + if (cur_page_footnotes_h == 0) + cur_page_footnotes_h = footnote_margin; + cur_page_footnotes_h += note_height; + } + // Note: we don't handle individual footnote lines larger than page height (which + // must be rare), but it looks like we'll just make with this line a page larger + // than screen height, that will then be truncated when rendered. Next page will + // continue with the remaining footnotes or content lines - which looks fine. + } + + void addLinesFootnotesToPage(int start, int end) { + for ( int i=start; i <= end; i++ ) { + LVRendLineInfo * line = lines[i]; + if ( !line->getLinks() ) + continue; + for ( int j=0; jgetLinks()->length(); j++ ) { + LVFootNote* note = line->getLinks()->get(j); + if ( note->empty() ) + continue; + if ( cur_page_seen_footnotes.indexOf(note) >= 0 ) + continue; // Already shown on this page + if ( !delayed_footnotes.empty() ) { + // Already some delayed footnotes + if ( delayed_footnotes.indexOf(note) < 0 ) + delayed_footnotes.add( note ); + continue; + } + if ( cur_page_nb_footnotes_lines == 0 ) { + // See if a first footnote line + its top margin fit on the page. + // If they don't, delay all footnotes but don't flush the page, + // as some main content line could still fit on this page. + if ( note->getLines()[0]->getHeight() > getAvailableHeightForFootnotes() ) { + if ( delayed_footnotes.indexOf(note) < 0 ) + delayed_footnotes.add( note ); + continue; + } + // Note: this could be made even more generic, delaying 2++ lines + // of a footnote and 2++ footnotes in case of tall footnotes lines + // (images? tables? which must be rare) + } + // We can proceed adding this footnote to the page (and flushing pages, + // continuing a footnote or adding others on new pages) + addFootnoteToPage( note ); + } + } + } + + void splitToPages() { + // A "line" is a slice of the document, it's a unit of what can be stacked + // into pages. It has a y coordinate as start and an other at end, + // that make its height. + // It's usually a line of text, or an image, but we also have one + // for each non-zero vertical margin, border and padding above and + // below block elements. + // A single "line" can also include multiple lines of text that have to + // be considered as a slice-unit for technical reasons: this happens with + // table rows (TR), so a table row will usually not be splitted among + // multiple pages, but pushed to the next page (except when a single row + // can't fit on a page: we'll then split inside that unit of slice). + if (nb_lines == 0) + return; + // Some of these lines can be flagged to indicate they should stick + // together: if not enough room at a bottom of a page, they can force + // a new page so they have more room at the top of the next page. This + // includes: top/bottom margins/borders/padding around blocks, lines of + // text with explicite page-break-inside: avoid, lines of text when CSS + // widows/orphans, lines of text surrounded by floats spanning them... + // + // Check for and handle lines per group of non-breakable lines + int start = 0; + for ( int i=0; i < nb_lines; i++ ) { + #ifdef DEBUG_PAGESPLIT + LVRendLineInfo * line = lines[i]; + printf("PS: line %d>%d h=%d, flags=<%d|%d>\n", + line->getStart(), line->getEnd(), line->getHeight(), + line->getSplitBefore(), line->getSplitAfter()); + #endif + bool force_new_page = false; + bool is_last_line = i == nb_lines-1; + int nxt_line_start = 0; + if ( !is_last_line ) { + LVRendLineInfo * cur_line = lines[i]; + LVRendLineInfo * nxt_line = lines[i+1]; + int cur_after = cur_line->getSplitAfter(); + int nxt_before = nxt_line->getSplitBefore(); + if ( cur_after==RN_SPLIT_AVOID || nxt_before==RN_SPLIT_AVOID) { + // Should stick together + continue; + } + nxt_line_start = nxt_line->getStart(); + if ( nxt_line_start < cur_line->getEnd() ) { + // Next line has some backward part that overlaps with current line: + // avoid a split between them + continue; + // Note: we may have later lines that could start backward and + // overlap with what we have already pushed... Nothing much we can + // do except looking way ahead here, which feels excessive. + // This should be rare though. + } + force_new_page = cur_after==RN_SPLIT_ALWAYS || nxt_before==RN_SPLIT_ALWAYS; + // (any AVOID has precedence over ALWAYS) + } + // We're allowed to break after cur_line (most often, we + // have start==i and we're just adding a single line). + addLinesToPage(start, i); + start = i+1; + if ( force_new_page && nxt_line_start < cur_page_bottom ) { + // Next line is backward and overlaps with what has been put + // on current page: cancel the forced page break + force_new_page = false; + // Note: it might feel ok to still make a new page, and (below + // after flushCurrentPage()) adjust the new page top to start + // at next_line_start - but in some heavy negative margins + // conditions, we might see some amount of the same text + // content on 2 pages, which feels really odd. + } + if ( force_new_page || is_last_line ) { + flushCurrentPage(); + } + } + } +}; +#endif // !USE_LEGACY_PAGESPLITTER void LVRendPageContext::split() { if ( !page_list ) return; - PageSplitState s(page_list, page_h, doc_font_size); #ifdef DEBUG_PAGESPLIT printf("PS: splitting lines into pages, page height=%d\n", page_h); #endif - int lineCount = lines.length(); +#ifndef USE_LEGACY_PAGESPLITTER + PageSplitState2 s(page_list, lines, page_h, doc_font_size); + s.splitToPages(); +#else + PageSplitState s(page_list, page_h, doc_font_size); + + int lineCount = lines.length(); LVRendLineInfo * line = NULL; for ( int lindex=0; lindex