diff --git a/crengine/include/cssdef.h b/crengine/include/cssdef.h index 8507e64b0..cb8b92e96 100644 --- a/crengine/include/cssdef.h +++ b/crengine/include/cssdef.h @@ -347,6 +347,9 @@ enum css_generic_value_t { // (it could have been a non-standard named value for line-height:, but we want to be // able to not override existing line-height: values) #define CSS_CR_HINT_STRUT_CONFINED 0x00000002 // -cr-hint: strut-confined (inheritable) +// Glyphs should not overflow line edges, and across text nodes or over images +// (it can also be used to disable hanging punctuation on the targeted nodes). +#define CSS_CR_HINT_FIT_GLYPHS 0x00000004 // -cr-hint: fit-glyphs (inheritable) // A node with these should be considered as TOC item of level N when building alternate TOC #define CSS_CR_HINT_TOC_LEVEL1 0x00000100 // -cr-hint: toc-level1 @@ -380,7 +383,7 @@ enum css_generic_value_t { // everything else indicates it is) // A few of them are inheritable, most are not. -#define CSS_CR_HINT_INHERITABLE_MASK 0x00000002 +#define CSS_CR_HINT_INHERITABLE_MASK 0x00000006 // Macro for easier checking #define STYLE_HAS_CR_HINT(s, h) ( (bool)(s->cr_hint.value & CSS_CR_HINT_##h) ) diff --git a/crengine/include/lvrend.h b/crengine/include/lvrend.h index 5926dc9db..0e72de34e 100644 --- a/crengine/include/lvrend.h +++ b/crengine/include/lvrend.h @@ -92,7 +92,7 @@ class BlockFloatFootprint { // Floats to transfer to final block for it to // start with these 5 "fake" embedded floats int floats_cnt; - int floats[5][5]; // max 5 floats, with (x,y,w,h,is_right) each + int floats[5][6]; // max 5 floats, with (x,y,w,h,is_right,inward_margin) each int getFinalMinY() { return used_min_y; }; int getFinalMaxY() { return used_max_y; }; @@ -127,9 +127,11 @@ lUInt32 styleToTextFmtFlags( bool is_block, const css_style_ref_t & style, lUInt void renderFinalBlock( ldomNode * node, LFormattedText * txform, RenderRectAccessor * fmt, lUInt32 & flags, int indent, int line_h, TextLangCfg * lang_cfg=NULL, int valign_dy=0, bool * is_link_start=NULL ); /// renders block which contains subblocks (with gRenderBlockRenderingFlags as flags) -int renderBlockElement( LVRendPageContext & context, ldomNode * enode, int x, int y, int width, int direction=REND_DIRECTION_UNSET, int * baseline=NULL ); +int renderBlockElement( LVRendPageContext & context, ldomNode * enode, int x, int y, int width, + int usable_left_overflow=0, int usable_right_overflow=0, int direction=REND_DIRECTION_UNSET, int * baseline=NULL ); /// renders block which contains subblocks -int renderBlockElement( LVRendPageContext & context, ldomNode * enode, int x, int y, int width, int direction, int * baseline, int rend_flags ); +int renderBlockElement( LVRendPageContext & context, ldomNode * enode, int x, int y, int width, + int usable_left_overflow, int usable_right_overflow, int direction, int * baseline, int rend_flags ); /// renders table element int renderTable( LVRendPageContext & context, ldomNode * element, int x, int y, int width, bool shrink_to_fit, int & fitted_width, int direction=REND_DIRECTION_UNSET, @@ -173,6 +175,8 @@ extern int gRenderDPI; extern bool gRenderScaleFontWithDPI; extern int gRootFontSize; +extern bool gHangingPunctuationEnabled; + #define INTERLINE_SCALE_FACTOR_NO_SCALE 1024 #define INTERLINE_SCALE_FACTOR_SHIFT 10 extern int gInterlineScaleFactor; diff --git a/crengine/include/lvstyles.h b/crengine/include/lvstyles.h index 16f71ff64..d36e3c2d3 100644 --- a/crengine/include/lvstyles.h +++ b/crengine/include/lvstyles.h @@ -321,6 +321,9 @@ class lvdomElementFormatRec { // is also limited to that for being carried in lInt16 slots when // formatting text in lvtextfm.cpp.) + short _usable_left_overflow; // Usable overflow for hanging punctuation + short _usable_right_overflow; // and glyphs negative side bearings + // Children blocks should be fully contained in their parent block, // and sibling nodes blocks should not overlap with other siblings, // except when float are involved and we allow them to continue @@ -351,18 +354,19 @@ class lvdomElementFormatRec { int _listprop_node_idx; // dataIndex of the UL/OL node this erm_final block // should get its marker from - // Added for padding from 15 to 16 32-bits ints - int _available1; + // We're now at 16 32-bits ints. If needing new fields, add some padding: + // Added for padding from 17 to 20 32-bits ints + // int _available1; int _available2; int _available3; public: lvdomElementFormatRec() : _x(0), _width(0), _y(0), _height(0) , _inner_width(0), _inner_x(0), _inner_y(0), _baseline(0) + , _usable_left_overflow(0), _usable_right_overflow(0) , _top_overflow(0), _bottom_overflow(0) , _lang_node_idx(0) , _listprop_node_idx(0) , _flags(0), _extra0(0) , _extra1(0), _extra2(0), _extra3(0), _extra4(0), _extra5(0) - , _available1(0) { } ~lvdomElementFormatRec() @@ -372,23 +376,23 @@ class lvdomElementFormatRec { { _x = _width = _y = _height = 0; _inner_width = _inner_x = _inner_y = _baseline = 0; + _usable_left_overflow = _usable_right_overflow = 0; _top_overflow = _bottom_overflow = 0; _lang_node_idx = _listprop_node_idx = 0; _flags = _extra0 = 0; _extra1 = _extra2 = _extra3 = _extra4 = _extra5 = 0; - _available1 = 0; } bool operator == ( lvdomElementFormatRec & v ) { return (_height==v._height && _y==v._y && _width==v._width && _x==v._x && _inner_width==v._inner_width && _inner_x==v._inner_x && _inner_y==v._inner_y && _baseline==v._baseline && + _usable_left_overflow==v._usable_left_overflow && _usable_right_overflow==v._usable_right_overflow && _top_overflow==v._top_overflow && _bottom_overflow==v._bottom_overflow && _lang_node_idx==v._lang_node_idx && _listprop_node_idx==v._listprop_node_idx && _flags==v._flags && _extra0==v._extra0 && _extra1==v._extra1 && _extra2==v._extra2 && _extra3==v._extra3 && - _extra4==v._extra4 && _extra5==v._extra5 && - _available1==v._available1 + _extra4==v._extra4 && _extra5==v._extra5 ); } bool operator != ( lvdomElementFormatRec & v ) @@ -396,12 +400,12 @@ class lvdomElementFormatRec { return (_height!=v._height || _y!=v._y || _width!=v._width || _x!=v._x || _inner_width!=v._inner_width || _inner_x!=v._inner_x || _inner_y!=v._inner_y || _baseline!=v._baseline || + _usable_left_overflow!=v._usable_left_overflow || _usable_right_overflow!=v._usable_right_overflow || _top_overflow!=v._top_overflow || _bottom_overflow!=v._bottom_overflow || _lang_node_idx!=v._lang_node_idx || _listprop_node_idx!=v._listprop_node_idx || _flags!=v._flags || _extra0!=v._extra0 || _extra1!=v._extra1 || _extra2!=v._extra2 || _extra3!=v._extra3 || - _extra4!=v._extra4 || _extra5!=v._extra5 || - _available1!=v._available1 + _extra4!=v._extra4 || _extra5!=v._extra5 ); } // Get/Set diff --git a/crengine/include/lvtextfm.h b/crengine/include/lvtextfm.h index 5a3845462..b6d71e868 100644 --- a/crengine/include/lvtextfm.h +++ b/crengine/include/lvtextfm.h @@ -19,9 +19,6 @@ #include "lvbmpbuf.h" #include "textlang.h" -// comment out following line to use old formatter -#define USE_NEW_FORMATTER 1 - #ifdef __cplusplus extern "C" { #endif @@ -81,7 +78,8 @@ extern "C" { #define LTEXT_SRC_IS_CLEAR_BOTH 0x03000000 // text follows
#define LTEXT_SRC_IS_CLEAR_LAST 0x04000000 // ignorable text, added when nothing follows
-#define LTEXT__AVAILABLE_BIT_28__ 0x08000000 +#define LTEXT_FIT_GLYPHS 0x08000000 // Avoid glyph overflows and override at line edges and between text nodes + #define LTEXT__AVAILABLE_BIT_29__ 0x10000000 #define LTEXT__AVAILABLE_BIT_30__ 0x20000000 #define LTEXT__AVAILABLE_BIT_31__ 0x40000000 @@ -212,7 +210,8 @@ typedef struct lInt32 y; /**< start y position of float */ lInt16 x; /**< start x position */ lUInt16 width; /**< width */ - lUInt16 height; /**< height */ + lUInt32 height; /**< height */ + lUInt16 inward_margin; /**< inward margin */ css_clear_t clear; /**< clear: css property value */ bool is_right; /**< is float: right */ bool to_position; /**< not yet positionned */ @@ -432,6 +431,8 @@ class LFormattedText lUInt32 Format(lUInt16 width, lUInt16 page_height, int para_direction=0, // = REND_DIRECTION_UNSET in lvrend.h + int usable_left_overflow=0, int usable_right_overflow=0, + bool hanging_punctuation=false, BlockFloatFootprint * float_footprint = NULL ); int GetSrcCount() @@ -481,6 +482,4 @@ class LFormattedText #endif -extern bool gFlgFloatingPunctuationEnabled; - #endif diff --git a/crengine/include/lvtinydom.h b/crengine/include/lvtinydom.h index c045c305f..e50381791 100644 --- a/crengine/include/lvtinydom.h +++ b/crengine/include/lvtinydom.h @@ -723,6 +723,11 @@ class RenderRectAccessor : public lvdomElementFormatRec void setInnerY( int y ); void setInnerWidth( int w ); + int getUsableLeftOverflow(); + int getUsableRightOverflow(); + void setUsableLeftOverflow( int dx ); + void setUsableRightOverflow( int dx ); + int getTopOverflow(); int getBottomOverflow(); void setTopOverflow( int dy ); @@ -2514,9 +2519,12 @@ class ldomDocument : public lxmlDocBase #if BUILD_LITE!=1 bool isRendered() { return _rendered; } /// renders (formats) document in memory: returns true if re-rendering needed, false if not - virtual bool render( LVRendPageList * pages, LVDocViewCallback * callback, int width, int dy, bool showCover, int y0, font_ref_t def_font, int def_interline_space, CRPropRef props ); + virtual bool render( LVRendPageList * pages, LVDocViewCallback * callback, int width, int dy, + bool showCover, int y0, font_ref_t def_font, int def_interline_space, + CRPropRef props, int usable_left_overflow=0, int usable_right_overflow=0 ); /// set global rendering properties - virtual bool setRenderProps( int width, int dy, bool showCover, int y0, font_ref_t def_font, int def_interline_space, CRPropRef props ); + virtual bool setRenderProps( int width, int dy, bool showCover, int y0, font_ref_t def_font, + int def_interline_space, CRPropRef props ); #endif /// create xpointer from pointer string ldomXPointer createXPointer( const lString16 & xPointerStr ); diff --git a/crengine/include/textlang.h b/crengine/include/textlang.h index d22f69af9..69686ed00 100644 --- a/crengine/include/textlang.h +++ b/crengine/include/textlang.h @@ -142,6 +142,9 @@ class TextLangCfg lString16 & getOpeningQuote( bool update_level=true ); lString16 & getClosingQuote( bool update_level=true ); + int getHyphenHangingPercent(); + int getHangingPercent( bool right_hanging, bool & check_font, const lChar16 * text, int pos, int next_usable ); + #if USE_HARFBUZZ==1 hb_language_t getHBLanguage() const { return _hb_language; } #endif diff --git a/crengine/src/lvdocview.cpp b/crengine/src/lvdocview.cpp index 802790111..a130171a7 100644 --- a/crengine/src/lvdocview.cpp +++ b/crengine/src/lvdocview.cpp @@ -2779,7 +2779,8 @@ void LVDocView::Render(int dx, int dy, LVRendPageList * pages) { //CRLog::trace("calling render() for document %08X font=%08X", (unsigned int)m_doc, (unsigned int)m_font.get() ); bool did_rerender = m_doc->render(pages, isDocumentOpened() ? m_callback : NULL, dx, dy, m_showCover, m_showCover ? dy + m_pageMargins.bottom * 4 : 0, - m_font, m_def_interline_space, m_props); + m_font, m_def_interline_space, m_props, + m_pageMargins.left, m_pageMargins.right); #if 0 // For debugging lvpagesplitter.cpp (small books) @@ -6244,7 +6245,9 @@ CRPropRef LVDocView::propsApply(CRPropRef props) { if ((int)fontMan->GetHintingMode() != mode && mode>=0 && mode<=2) { //CRLog::debug("Setting hinting mode to %d", mode); fontMan->SetHintingMode((hinting_mode_t)mode); - requestRender(); // does m_doc->clearRendBlockCache(), which is needed on hinting mode change + REQUEST_RENDER("propsApply - font hinting") + // requestRender() does m_doc->clearRendBlockCache(), which is needed + // on hinting mode change } } else if (name == PROP_HIGHLIGHT_SELECTION_COLOR || name == PROP_HIGHLIGHT_BOOKMARK_COLOR_COMMENT || name == PROP_HIGHLIGHT_BOOKMARK_COLOR_COMMENT) { REQUEST_RENDER("propsApply - highlight") @@ -6260,7 +6263,7 @@ CRPropRef LVDocView::propsApply(CRPropRef props) { if ((int)fontMan->GetKerningMode() != mode && mode>=0 && mode<=3) { //CRLog::debug("Setting kerning mode to %d", mode); fontMan->SetKerningMode((kerning_mode_t)mode); - requestRender(); + REQUEST_RENDER("propsApply - font kerning") } } else if (name == PROP_FONT_WEIGHT_EMBOLDEN) { bool embolden = props->getBoolDef(PROP_FONT_WEIGHT_EMBOLDEN, false); @@ -6463,9 +6466,11 @@ CRPropRef LVDocView::propsApply(CRPropRef props) { REQUEST_RENDER("propsApply footnotes") } else if (name == PROP_FLOATING_PUNCTUATION) { bool value = props->getBoolDef(PROP_FLOATING_PUNCTUATION, true); - if ( gFlgFloatingPunctuationEnabled != value ) { - gFlgFloatingPunctuationEnabled = value; - REQUEST_RENDER("propsApply floating punct") + if ( gHangingPunctuationEnabled != value ) { + gHangingPunctuationEnabled = value; + REQUEST_RENDER("propsApply - hanging punctuation") + // requestRender() does m_doc->clearRendBlockCache(), which is needed + // on hanging punctuation change } } else if (name == PROP_RENDER_BLOCK_RENDERING_FLAGS) { int value = props->getIntDef(PROP_RENDER_BLOCK_RENDERING_FLAGS, DEF_RENDER_BLOCK_RENDERING_FLAGS); diff --git a/crengine/src/lvrend.cpp b/crengine/src/lvrend.cpp index 2cb46a7eb..01f937358 100644 --- a/crengine/src/lvrend.cpp +++ b/crengine/src/lvrend.cpp @@ -50,6 +50,8 @@ // crengine default used to be "width: 100%", but now that we // can shrink to fit, it is "width: auto". +bool gHangingPunctuationEnabled = false; + int gInterlineScaleFactor = INTERLINE_SCALE_FACTOR_NO_SCALE; int gRenderDPI = DEF_RENDER_DPI; // if 0: old crengine behaviour: 1px/pt=1px, 1in/cm/pc...=0px @@ -1460,8 +1462,10 @@ class CCRTable { LFormattedTextRef txform; int em = cell->elem->getFont()->getSize(); css_style_ref_t elem_style = cell->elem->getStyle(); - int padding_left = lengthToPx( elem_style->padding[0], cell->width, em ) + measureBorder(cell->elem,3); - int padding_right = lengthToPx( elem_style->padding[1], cell->width, em ) + measureBorder(cell->elem,1); + int border_left = measureBorder(cell->elem,3); + int border_right = measureBorder(cell->elem,1); + int padding_left = lengthToPx( elem_style->padding[0], cell->width, em ) + border_left; + int padding_right = lengthToPx( elem_style->padding[1], cell->width, em ) + border_right; int padding_top = lengthToPx( elem_style->padding[2], cell->width, em ) + measureBorder(cell->elem,0); int padding_bottom = lengthToPx( elem_style->padding[3], cell->width, em ) + measureBorder(cell->elem,2); RenderRectAccessor fmt( cell->elem ); @@ -1473,6 +1477,8 @@ class CCRTable { fmt.setInnerX( padding_left ); fmt.setInnerY( padding_top ); fmt.setInnerWidth( cell->width - padding_left - padding_right ); + fmt.setUsableLeftOverflow( padding_left - border_left ); + fmt.setUsableRightOverflow( padding_right - border_right ); RENDER_RECT_SET_FLAG(fmt, INNER_FIELDS_SET); RENDER_RECT_SET_DIRECTION(fmt, cell->direction); fmt.setLangNodeIndex( TextLangMan::getLangNodeIndex(cell->elem) ); @@ -1592,6 +1598,7 @@ class CCRTable { baseline = REQ_BASELINE_FOR_TABLE; } int h = renderBlockElement( *cell_context, cell->elem, 0, 0, cell->width, + 0, 0, // no usable left/right overflow outside cell cell->direction, &baseline, rend_flags); cell->height = h; if ( cell->valign == 0 ) { // vertical-align: baseline @@ -2222,7 +2229,8 @@ lUInt32 styleToTextFmtFlags( bool is_block, const css_style_ref_t & style, lUInt flg |= LTEXT_FLAG_PREFORMATTED; if ( style->white_space == css_ws_nowrap ) // white-space: nowrap flg |= LTEXT_FLAG_NOWRAP; - //flg |= oldflags & ~LTEXT_FLAG_NEWLINE; + if ( STYLE_HAS_CR_HINT(style, FIT_GLYPHS) ) // glyph fitting via -cr-hint + flg |= LTEXT_FIT_GLYPHS; return flg; } @@ -2630,6 +2638,8 @@ void renderFinalBlock( ldomNode * enode, LFormattedText * txform, RenderRectAcce // it is set to a negative value (the width of the marker), so to handle text // indentation from the outside marker just like regular negative text-indent. // So, sadly, let's keep it that way to not break legacy rendering. + // todo: pass indent via txform->setTextIndent() (like we do for the strut + // below, and get rid of it in AddSourceLine()) indent = lengthToPx(style->text_indent, width, em); // lvstsheet sets the lowest bit to 1 when text-indent has the "hanging" keyword: if ( style->text_indent.value & 0x00000001 ) { @@ -4184,6 +4194,7 @@ int renderBlockElementLegacy( LVRendPageContext & context, ldomNode * enode, int fmt.setX( fmt.getX() ); fmt.setY( fmt.getY() ); fmt.setLangNodeIndex( 0 ); // No support for lang in legacy rendering + // (No support for overflows and hanging punctuation in legacy mode) fmt.push(); //if ( CRLog::isTraceEnabled() ) // CRLog::trace("rendering final node: %s %d %s", LCSTR(enode->getNodeName()), enode->getDataIndex(), LCSTR(ldomXPointer(enode,0).toString()) ); @@ -4415,25 +4426,31 @@ class FlowState { lInt32 lang_node_idx; int x_min; int x_max; + int usable_overflow_x_min; + int usable_overflow_x_max; int l_y; int in_y_min; int in_y_max; bool avoid_pb_inside; - void reset(int dir, lInt32 langNodeIdx, int xmin, int xmax, int ly, int iymin, int iymax, bool avoidpbinside) { + void reset(int dir, lInt32 langNodeIdx, int xmin, int xmax, int overxmin, int overxmax, int ly, int iymin, int iymax, bool avoidpbinside) { direction = dir; lang_node_idx = langNodeIdx; x_min = xmin; x_max = xmax; + usable_overflow_x_min = overxmin; + usable_overflow_x_max = overxmax; l_y = ly; in_y_min = iymin; in_y_max = iymax; avoid_pb_inside = avoidpbinside; } - BlockShift(int dir, lInt32 langNodeIdx, int xmin, int xmax, int ly, int iymin, int iymax, bool avoidpbinside) : + BlockShift(int dir, lInt32 langNodeIdx, int xmin, int xmax, int overxmin, int overxmax, int ly, int iymin, int iymax, bool avoidpbinside) : direction(dir), lang_node_idx(langNodeIdx), x_min(xmin), x_max(xmax), + usable_overflow_x_min(overxmin), + usable_overflow_x_max(overxmax), l_y(ly), in_y_min(iymin), in_y_max(iymax), @@ -4444,16 +4461,32 @@ class FlowState { public: ldomNode * node; int level; // level that owns this float + int inward_margin; // inner margin (left margin for right floats, right margin for left floats), + // 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) 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) : lvRect(x0,y0,x1,y1), level(l), + inward_margin(0), is_right(r), final_pos(f), node(n) - { } + { + if (n && n->getChildCount() > 0) { + // The margins were used to position the original + // float node in its wrapping floatBox - so get it + // back from their relative positions + RenderRectAccessor fmt(n->getChildNode(0)); + if (is_right) + inward_margin = fmt.getX(); + else + inward_margin = (x1 - x0) - (fmt.getX() + fmt.getWidth()); + } + } }; int direction; // flow inline direction (LTR/RTL) lInt32 lang_node_idx; // dataIndex of nearest upper node with a lang="" attribute (0 if none) @@ -4473,6 +4506,8 @@ class FlowState { int in_y_max; // that overflow this level height) int x_min; // current left min x int x_max; // current right max x + int usable_overflow_x_min; // current left and right x usable for glyph overflows and hanging punctuation, + int usable_overflow_x_max; // reset when some border or background color change is met int baseline_req; // baseline type requested (REQ_BASELINE_FOR_INLINE_BLOCK or REQ_BASELINE_FOR_TABLE) int baseline_y; // baseline y relative to formatting context top (computed when rendering inline-block/table) bool baseline_set; // (set to true on first baseline met) @@ -4496,7 +4531,8 @@ class FlowState { int vm_back_usable_as_margin; // previously moved vertical space where next margin could be accounted in public: - FlowState( LVRendPageContext & ctx, int width, int rendflags, int dir=REND_DIRECTION_UNSET, lInt32 langNodeIdx=0 ): + FlowState( LVRendPageContext & ctx, int width, int usable_left_overflow, int usable_right_overflow, + int rendflags, int dir=REND_DIRECTION_UNSET, lInt32 langNodeIdx=0 ): direction(dir), lang_node_idx(langNodeIdx), context(ctx), @@ -4537,6 +4573,8 @@ class FlowState { } top_clear_level = is_main_flow ? 1 : 2; // see resetFloatsLevelToTopLevel() page_height = context.getPageHeight(); + usable_overflow_x_min = x_min - usable_left_overflow; + usable_overflow_x_max = x_max + usable_right_overflow; } ~FlowState() { // Shouldn't be needed as these must have been cleared @@ -4589,6 +4627,12 @@ class FlowState { bool getAvoidPbInside() { return avoid_pb_inside; } + int getUsableLeftOverflow() { + return x_min - usable_overflow_x_min; + } + int getUsableRightOverflow() { + return usable_overflow_x_max - x_max; + } void setRequestedBaselineType(int baseline_req_type) { baseline_req = baseline_req_type; @@ -5320,20 +5364,29 @@ class FlowState { // Enter/leave a block level: backup/restore some of this FlowState // fields, and do some housekeeping. - void newBlockLevel( int width, int d_left, bool avoid_pb, int dir, lInt32 langNodeIdx ) { + void newBlockLevel( int width, int d_left, int usable_overflow_reset_left, int usable_overflow_reset_right, + bool avoid_pb, int dir, lInt32 langNodeIdx ) { // Don't new/delete to avoid too many malloc/free, keep and re-use/reset // the ones already created if ( _shifts.length() <= level ) { - _shifts.push( new BlockShift( direction, lang_node_idx, x_min, x_max, l_y, in_y_min, in_y_max, avoid_pb_inside ) ); + _shifts.push( new BlockShift( direction, lang_node_idx, + x_min, x_max, usable_overflow_x_min, usable_overflow_x_max, + l_y, in_y_min, in_y_max, avoid_pb_inside ) ); } else { - _shifts[level]->reset( direction, lang_node_idx, x_min, x_max, l_y, in_y_min, in_y_max, avoid_pb_inside ); + _shifts[level]->reset( direction, lang_node_idx, + x_min, x_max, usable_overflow_x_min, usable_overflow_x_max, + l_y, in_y_min, in_y_max, avoid_pb_inside ); } direction = dir; if (langNodeIdx != -1) lang_node_idx = langNodeIdx; x_min += d_left; x_max = x_min + width; + if ( usable_overflow_reset_left >= 0 ) // -1 means: don't reset, keep previous level limits + usable_overflow_x_min = x_min - usable_overflow_reset_left; + if ( usable_overflow_reset_right >= 0 ) + usable_overflow_x_max = x_max + usable_overflow_reset_right; l_y = c_y; in_y_min = c_y; in_y_max = c_y; @@ -5355,6 +5408,8 @@ class FlowState { lang_node_idx = prev->lang_node_idx; x_min = prev->x_min; x_max = prev->x_max; + usable_overflow_x_min = prev->usable_overflow_x_min; + usable_overflow_x_max = prev->usable_overflow_x_max; l_y = prev->l_y; in_y_min = in_y_min < prev->in_y_min ? in_y_min : prev->in_y_min; // keep sublevel's one if smaller in_y_max = in_y_max > prev->in_y_max ? in_y_max : prev->in_y_max; // keep sublevel's one if larger @@ -5765,6 +5820,7 @@ class FlowState { footprint.floats[floats_involved][2] = x1 - x0; // width footprint.floats[floats_involved][3] = y1 - y0; // height footprint.floats[floats_involved][4] = flt->is_right; + footprint.floats[floats_involved][5] = flt->inward_margin; } floats_involved++; } @@ -5895,7 +5951,17 @@ void BlockFloatFootprint::generateEmbeddedFloatsFromFloatIds( ldomNode * node, floats[floats_cnt][1] = y0; // y floats[floats_cnt][2] = x1 - x0; // width floats[floats_cnt][3] = y1 - y0; // height - floats[floats_cnt][4] = RENDER_RECT_HAS_FLAG(fmt, FLOATBOX_IS_RIGHT); // is_right + bool is_right = RENDER_RECT_HAS_FLAG(fmt, FLOATBOX_IS_RIGHT); + floats[floats_cnt][4] = is_right; + int inward_margin = 0; + if ( fbox->getChildCount() > 0 ) { + RenderRectAccessor fmt(fbox->getChildNode(0)); + if ( is_right ) + inward_margin = fmt.getX(); + else + inward_margin = (x1 - x0) - (fmt.getX() + fmt.getWidth()); + } + floats[floats_cnt][5] = inward_margin; /* Uncomment for checking reproducible results: if (x1 < x0) printf("!!!! %d %d %d %d\n", rc.left, rc.right, rc.top, rc.bottom); if ( bf0!=floats[floats_cnt][0] || bf1!=floats[floats_cnt][1] || bf2!=floats[floats_cnt][2] || @@ -5923,6 +5989,9 @@ void BlockFloatFootprint::generateEmbeddedFloatsFromFootprints( int final_width // which case they'll have no visual impact on the text), // just so we can clear them when a
// is met. + // Note: we give inward_margin=0 with fake floats (we + // could compute them, but we would need 2 other slots + // in RenderRectAccessor to store them, so let's not). // Top left rectangle if ( left_h > 0 ) { floats[floats_cnt][0] = 0; // x @@ -5930,6 +5999,7 @@ void BlockFloatFootprint::generateEmbeddedFloatsFromFootprints( int final_width floats[floats_cnt][2] = left_w; // width floats[floats_cnt][3] = left_h; // height floats[floats_cnt][4] = 0; // is_right + floats[floats_cnt][5] = 0; // inward_margin floats_cnt++; } // Top right rectangle @@ -5939,6 +6009,7 @@ void BlockFloatFootprint::generateEmbeddedFloatsFromFootprints( int final_width floats[floats_cnt][2] = right_w; // width floats[floats_cnt][3] = right_h; // height floats[floats_cnt][4] = 1; // is_right + floats[floats_cnt][5] = 0; // inward_margin floats_cnt++; } // Dummy 0x0 float for minimal y for next left float @@ -5948,6 +6019,7 @@ void BlockFloatFootprint::generateEmbeddedFloatsFromFootprints( int final_width floats[floats_cnt][2] = 0; // width floats[floats_cnt][3] = 0; // height floats[floats_cnt][4] = 0; // is_right + floats[floats_cnt][5] = 0; // inward_margin floats_cnt++; } // Dummy 0x0 float for minimal y for next right float @@ -5957,6 +6029,7 @@ void BlockFloatFootprint::generateEmbeddedFloatsFromFootprints( int final_width floats[floats_cnt][2] = 0; // width floats[floats_cnt][3] = 0; // height floats[floats_cnt][4] = 1; // is_right + floats[floats_cnt][5] = 0; // inward_margin floats_cnt++; } } @@ -6140,14 +6213,12 @@ void renderBlockElementEnhanced( FlowState * flow, ldomNode * enode, int x, int int em = enode->getFont()->getSize(); - int padding_left = lengthToPx( style->padding[0], container_width, em ) - + measureBorder(enode, 3) + DEBUG_TREE_DRAW; - int padding_right = lengthToPx( style->padding[1], container_width, em ) - + measureBorder(enode, 1) + DEBUG_TREE_DRAW; - int padding_top = lengthToPx( style->padding[2], container_width, em ) - + measureBorder(enode, 0) + DEBUG_TREE_DRAW; - int padding_bottom = lengthToPx( style->padding[3], container_width, em ) - + measureBorder(enode, 2) + DEBUG_TREE_DRAW; + int border_left = measureBorder(enode, 3); + int border_right = measureBorder(enode, 1); + int padding_left = lengthToPx( style->padding[0], container_width, em ) + border_left + DEBUG_TREE_DRAW; + int padding_right = lengthToPx( style->padding[1], container_width, em ) + border_right + DEBUG_TREE_DRAW; + int padding_top = lengthToPx( style->padding[2], container_width, em ) + measureBorder(enode, 0) + DEBUG_TREE_DRAW; + int padding_bottom = lengthToPx( style->padding[3], container_width, em ) + measureBorder(enode, 2) + DEBUG_TREE_DRAW; css_length_t css_margin_left = style->margin[0]; css_length_t css_margin_right = style->margin[1]; @@ -6812,11 +6883,32 @@ void renderBlockElementEnhanced( FlowState * flow, ldomNode * enode, int x, int // to renderBlockElement() even if it feels a bit out of place, // notably in the float positionning code. But it works... + // Update left and right overflows (usable by glyphs) if this node + // has some background or borders, to be given below to 'flow'. + int usable_overflow_reset_left = -1; + int usable_overflow_reset_right = -1; + if ( style->background_color.type == css_val_color || !style->background_image.empty() ) { + // New (or same) background color specified (we assume there is + // a color change): avoid glyphs/hanging punctuation from leaking + // over the background change. + usable_overflow_reset_left = padding_left; + usable_overflow_reset_right = padding_right; + } + // If there's some border, avoid glyphs/hanging punctuation from + // leaking on or over the border. + if ( border_left ) { + usable_overflow_reset_left = padding_left - border_left; + } + if ( border_right ) { + usable_overflow_reset_right = padding_right - border_right; + } + // Shrink flow state area: children that are float will be // constrained into this area // ('width' already had margin_left/_right substracted) flow->newBlockLevel(width - list_marker_padding - padding_left - padding_right, // width margin_left + (is_rtl ? 0 : list_marker_padding) + padding_left, // d_left + usable_overflow_reset_left, usable_overflow_reset_right, break_inside==RN_SPLIT_AVOID, direction, has_lang_attribute ? enode->getDataIndex() : -1); @@ -6874,8 +6966,12 @@ void renderBlockElementEnhanced( FlowState * flow, ldomNode * enode, int x, int // padding-left/right) for the flow to correctly position inner floats: // flow->addFloat() will additionally shift its positionning by the // child x/y set by this renderBlockElement(). + // We provide 0,0 as the usable left/right overflows, so no glyph/hanging + // punctuation will leak outside the floatBox - but the floatBox contains + // the initial float element's margins, which can then be used if it has + // no border (if borders, only the padding can be used). renderBlockElement( alt_context, child, (is_rtl ? 0 : list_marker_padding) + padding_left, - padding_top, width - list_marker_padding - padding_left - padding_right, direction ); + padding_top, 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 lString16Collection * link_ids = alt_context.getLinkIds(); @@ -7158,6 +7254,27 @@ void renderBlockElementEnhanced( FlowState * flow, ldomNode * enode, int x, int fmt.setInnerY( padding_top ); fmt.setInnerWidth( inner_width ); RENDER_RECT_SET_FLAG(fmt, INNER_FIELDS_SET); + // Usable overflow for glyphs and hanging punctuation + int usable_overflow_left = flow->getUsableLeftOverflow() + margin_left; + int usable_overflow_right = flow->getUsableRightOverflow() + margin_right; + if ( style->background_color.type == css_val_color || !style->background_image.empty() ) { + // New (or same) background color specified (we assume there is + // a color change): avoid glyphs/hanging punctuation from leaking + // over the background change. + usable_overflow_left = padding_left; + usable_overflow_right = padding_right; + } + // If there's some border, avoid glyphs/hanging punctuation from + // leaking on or over the border. + if ( border_left ) { + usable_overflow_left = padding_left - border_left; + } + if ( border_right ) { + usable_overflow_right = padding_right - border_right; + } + fmt.setUsableLeftOverflow( usable_overflow_left ); + fmt.setUsableRightOverflow( usable_overflow_right ); + // Done with updating RenderRectAccessor fields, have them saved fmt.push(); // (These setInner* needs to be set before creating float_footprint if // we want to debug/valide floatIds coordinates) @@ -7381,7 +7498,8 @@ void renderBlockElementEnhanced( FlowState * flow, ldomNode * enode, int x, int } // Entry points for rendering the root node, a table cell or a float -int renderBlockElement( LVRendPageContext & context, ldomNode * enode, int x, int y, int width, int direction, int * baseline, int rend_flags ) +int renderBlockElement( LVRendPageContext & context, ldomNode * enode, int x, int y, int width, + int usable_left_overflow, int usable_right_overflow, int direction, int * baseline, int rend_flags ) { if ( BLOCK_RENDERING(rend_flags, ENHANCED) ) { // Create a flow state (aka "block formatting context") for the rendering @@ -7389,7 +7507,8 @@ int renderBlockElement( LVRendPageContext & context, ldomNode * enode, int x, in // (We are called when rendering the root node, and when rendering each float // met along walking the root node hierarchy - and when meeting a new float // in a float, etc...) - FlowState flow( context, width, rend_flags, direction, TextLangMan::getLangNodeIndex(enode) ); + FlowState flow( context, width, usable_left_overflow, usable_right_overflow, rend_flags, + direction, TextLangMan::getLangNodeIndex(enode) ); if (baseline != NULL) { flow.setRequestedBaselineType(*baseline); } @@ -7407,11 +7526,13 @@ int renderBlockElement( LVRendPageContext & context, ldomNode * enode, int x, in return renderBlockElementLegacy( context, enode, x, y, width); } } -int renderBlockElement( LVRendPageContext & context, ldomNode * enode, int x, int y, int width, int direction, int * baseline ) +int renderBlockElement( LVRendPageContext & context, ldomNode * enode, int x, int y, int width, + int usable_left_overflow, int usable_right_overflow, int direction, int * baseline ) { // Use global rendering flags // Note: we're not currently using it with other flags that the global ones. - return renderBlockElement( context, enode, x, y, width, direction, baseline, gRenderBlockRenderingFlags ); + return renderBlockElement( context, enode, x, y, width, usable_left_overflow, usable_right_overflow, + direction, baseline, gRenderBlockRenderingFlags ); } //draw border lines,support color,width,all styles, not support border-collapse @@ -9270,6 +9391,10 @@ void getRenderedWidths(ldomNode * node, int &maxWidth, int &minWidth, int direct // Start measurements and recursions: getRenderedWidths(node, maxWidth, minWidth, direction, ignoreMargin, rendFlags, curMaxWidth, curWordWidth, collapseNextSpace, lastSpaceWidth, indent, NULL, false, isStartNode); + // We took more care with including side bearings into minWidth when considering + // single words, than into maxWidth: so trust minWidth if larger than maxWidth. + if ( maxWidth < minWidth) + maxWidth = minWidth; } void getRenderedWidths(ldomNode * node, int &maxWidth, int &minWidth, int direction, bool ignoreMargin, int rendFlags, @@ -9889,6 +10014,22 @@ void getRenderedWidths(ldomNode * node, int &maxWidth, int &minWidth, int direct bool nowrap = (parent_style->white_space == css_ws_nowrap) || (parent_style->white_space == css_ws_pre); bool pre = parent_style->white_space >= css_ws_pre; int space_width_scale_percent = pre ? 100 : parent->getDocument()->getSpaceWidthScalePercent(); + + // If fit_glyphs, we'll adjust below each word width with calls to + // getLeftSideBearing() and getRightSideBearing(). These should be + // called with the exact same parameters as used in lvtextfm.cpp + // addLine(). (Previously, we adjusted overflows and underflows on + // the left, and only overflows on the right. We now only adjust + // overflows on both sides - but don't touch underflows to keep + // the text natural alignment.) + // bool fit_glyphs = STYLE_HAS_CR_HINT(parent_style, FIT_GLYPHS); + // + // Best to always measure accounting for overflows: we don't know + // what adjusments lvtextfm.cpp AddLine() will do depending on + // the usable_left/right_overflows it got. + // (Let's keep this easily toggable in case we need it.) + #define fit_glyphs true + // measure text const lChar16 * txt = text.c_str(); #ifdef DEBUG_GETRENDEREDWIDTHS @@ -9898,10 +10039,7 @@ void getRenderedWidths(ldomNode * node, int &maxWidth, int &minWidth, int direct #define MAX_TEXT_CHUNK_SIZE 4096 static lUInt16 widths[MAX_TEXT_CHUNK_SIZE+1]; static lUInt8 flags[MAX_TEXT_CHUNK_SIZE+1]; - // We adjust below each word width with calls to getLeftSideBearing() - // and getRightSideBearing(). These should be called with the exact same - // parameters as used in lvtextfm.cpp getAdditionalCharWidth() and - // getAdditionalCharWidthOnLeft(). + // todo: use fribidi and split measurement at fribidi level change, // and beware left/right side bearing adjustments... #if (USE_LIBUNIBREAK==1) @@ -9957,11 +10095,11 @@ void getRenderedWidths(ldomNode * node, int &maxWidth, int &minWidth, int direct collapseNextSpace = true; // ignore next spaces, even if in another node lastSpaceWidth = pre ? 0 : w; // Don't remove last space width if 'pre' curMaxWidth += w; // add this space to non-wrap width - if (curWordWidth > 0) { // there was a word before this space + if (fit_glyphs && curWordWidth > 0) { // there was a word before this space if (start+i > 0) { // adjust for last word's last char overflow (italic, letter f...) lChar16 prevc = *(txt + start + i - 1); - int right_overflow = - font->getRightSideBearing(prevc, true, true); + int right_overflow = - font->getRightSideBearing(prevc, true); curWordWidth += right_overflow; } } @@ -9973,31 +10111,33 @@ void getRenderedWidths(ldomNode * node, int &maxWidth, int &minWidth, int direct collapseNextSpace = false; // next space should not be ignored lastSpaceWidth = 0; // no width to take off if we stop with this char curMaxWidth += w; - if (curWordWidth > 0) { // there was a word or CJK char before this CJK char + if (fit_glyphs && curWordWidth > 0) { // there was a word or CJK char before this CJK char if (start+i > 0) { // adjust for last word's last char or previous CJK char right overflow lChar16 prevc = *(txt + start + i - 1); - int right_overflow = - font->getRightSideBearing(prevc, true, true); + int right_overflow = - font->getRightSideBearing(prevc, true); curWordWidth += right_overflow; } } if (curWordWidth > minWidth) // done with previous word minWidth = curWordWidth; // longest word found curWordWidth = w; - // adjust for leading overflow - int left_overflow = - font->getLeftSideBearing(c, false, true); - curWordWidth += left_overflow; - if (start + i == 0) // at start of text only? (not sure) - curMaxWidth += left_overflow; // also add it to max width + if (fit_glyphs) { + // adjust for leading overflow + int left_overflow = - font->getLeftSideBearing(c, true); + curWordWidth += left_overflow; + if (start + i == 0) // at start of text only? (not sure) + curMaxWidth += left_overflow; // also add it to max width + } } } else if (brk == LINEBREAK_MUSTBREAK) { // \n if pre // Get done with current word - if (curWordWidth > 0) { // we end with a word + if (fit_glyphs && curWordWidth > 0) { // we end with a word if (start+i > 0) { // adjust for last word's last char or previous CJK char right overflow lChar16 prevc = *(txt + start + i - 1); - int right_overflow = - font->getRightSideBearing(prevc, true, true); + int right_overflow = - font->getRightSideBearing(prevc, true); curWordWidth += right_overflow; curMaxWidth += right_overflow; } @@ -10029,9 +10169,9 @@ void getRenderedWidths(ldomNode * node, int &maxWidth, int &minWidth, int direct collapseNextSpace = false; // next space should not be ignored lastSpaceWidth = 0; // no width to take off if we stop with this char } - if (curWordWidth == 0) { // first char of a word + if (fit_glyphs && curWordWidth == 0) { // first char of a word // adjust for leading overflow on first char of a word - int left_overflow = - font->getLeftSideBearing(c, false, true); + int left_overflow = - font->getLeftSideBearing(c, true); curWordWidth += left_overflow; if (start + i == 0) // at start of text only? (not sure) curMaxWidth += left_overflow; // also add it to max width @@ -10067,11 +10207,11 @@ void getRenderedWidths(ldomNode * node, int &maxWidth, int &minWidth, int direct collapseNextSpace = true; // ignore next spaces, even if in another node lastSpaceWidth = w; curMaxWidth += w; // add this space to non-wrap width - if (curWordWidth > 0) { // there was a word before this space + if (fit_glyphs && curWordWidth > 0) { // there was a word before this space if (start+i > 0) { // adjust for last word's last char overflow (italic, letter f...) lChar16 prevc = *(txt + start + i - 1); - int right_overflow = - font->getRightSideBearing(prevc, true, true); + int right_overflow = - font->getRightSideBearing(prevc, true); curWordWidth += right_overflow; } } @@ -10083,29 +10223,31 @@ void getRenderedWidths(ldomNode * node, int &maxWidth, int &minWidth, int direct collapseNextSpace = false; // next space should not be ignored lastSpaceWidth = 0; // no width to take off if we stop with this char curMaxWidth += w; - if (curWordWidth > 0) { // there was a word or CJK char before this CJK char + if (fit_glyphs && curWordWidth > 0) { // there was a word or CJK char before this CJK char if (start+i > 0) { // adjust for last word's last char or previous CJK char right overflow lChar16 prevc = *(txt + start + i - 1); - int right_overflow = - font->getRightSideBearing(prevc, true, true); + int right_overflow = - font->getRightSideBearing(prevc, true); curWordWidth += right_overflow; } } if (curWordWidth > minWidth) // done with previous word minWidth = curWordWidth; // longest word found curWordWidth = w; - // adjust for leading overflow - int left_overflow = - font->getLeftSideBearing(c, false, true); - curWordWidth += left_overflow; - if (start + i == 0) // at start of text only? (not sure) - curMaxWidth += left_overflow; // also add it to max width + if (fit_glyphs) { + // adjust for leading overflow + int left_overflow = - font->getLeftSideBearing(c, true); + curWordWidth += left_overflow; + if (start + i == 0) // at start of text only? (not sure) + curMaxWidth += left_overflow; // also add it to max width + } } else { // A char part of a word collapseNextSpace = false; // next space should not be ignored lastSpaceWidth = 0; // no width to take off if we stop with this char - if (curWordWidth == 0) { // first char of a word + if (fit_glyphs && curWordWidth == 0) { // first char of a word // adjust for leading overflow on first char of a word - int left_overflow = - font->getLeftSideBearing(c, false, true); + int left_overflow = - font->getLeftSideBearing(c, true); curWordWidth += left_overflow; if (start + i == 0) // at start of text only? (not sure) curMaxWidth += left_overflow; // also add it to max width @@ -10125,11 +10267,11 @@ void getRenderedWidths(ldomNode * node, int &maxWidth, int &minWidth, int direct } #endif // not USE_LIBUNIBREAK==1 if ( chars_measured == len ) { // done with this text node - if (curWordWidth > 0) { // we end with a word + if (fit_glyphs && curWordWidth > 0) { // we end with a word if (start+len > 0) { // adjust for word last char right overflow lChar16 prevc = *(txt + start + len - 1); - int right_overflow = - font->getRightSideBearing(prevc, true, true); + int right_overflow = - font->getRightSideBearing(prevc, true); curWordWidth += right_overflow; curMaxWidth += right_overflow; // also add it to max width } diff --git a/crengine/src/lvstsheet.cpp b/crengine/src/lvstsheet.cpp index e0abe3c2e..923ab16df 100644 --- a/crengine/src/lvstsheet.cpp +++ b/crengine/src/lvstsheet.cpp @@ -1952,6 +1952,7 @@ bool LVCssDeclaration::parse( const char * &decl, bool higher_importance, lxmlDo } else if ( substr_icompare("footnote-inpage", decl) ) hints |= CSS_CR_HINT_FOOTNOTE_INPAGE; else if ( substr_icompare("strut-confined", decl) ) hints |= CSS_CR_HINT_STRUT_CONFINED; + else if ( substr_icompare("fit-glyphs", decl) ) hints |= CSS_CR_HINT_FIT_GLYPHS; else if ( substr_icompare("text-selection-skip", decl) ) hints |= CSS_CR_HINT_TEXT_SELECTION_SKIP; else if ( substr_icompare("text-selection-inline", decl) ) hints |= CSS_CR_HINT_TEXT_SELECTION_INLINE; else if ( substr_icompare("text-selection-block", decl) ) hints |= CSS_CR_HINT_TEXT_SELECTION_BLOCK; diff --git a/crengine/src/lvtextfm.cpp b/crengine/src/lvtextfm.cpp index 019596b17..238bc257d 100644 --- a/crengine/src/lvtextfm.cpp +++ b/crengine/src/lvtextfm.cpp @@ -308,8 +308,6 @@ void lvtextAddSourceObject( #define DUMMY_IMAGE_SIZE 16 -bool gFlgFloatingPunctuationEnabled = true; - void LFormattedText::AddSourceObject( lUInt32 flags, /* flags */ lInt16 interval, /* line height in screen pixels */ @@ -417,6 +415,9 @@ class LVFormatter { bool m_no_clear_own_floats; bool m_allow_strut_confining; bool m_has_multiple_scripts; + int m_usable_left_overflow; + int m_usable_right_overflow; + bool m_hanging_punctuation; bool m_indent_first_line_done; int m_indent_after_first_line; int m_indent_current; @@ -465,6 +466,9 @@ class LVFormatter { m_has_ongoing_float = false; m_no_clear_own_floats = false; m_has_multiple_scripts = false; + m_usable_left_overflow = 0; + m_usable_right_overflow = 0; + m_hanging_punctuation = false; m_specified_para_dir = REND_DIRECTION_UNSET; #if (USE_FRIBIDI==1) m_bidi_ctypes = NULL; @@ -617,7 +621,9 @@ class LVFormatter { LVRendPageContext alt_context( NULL, m_pbuffer->page_height, false ); // We render the float with the specified direction (from upper dir=), even // if UNSET (and not with the direction determined by fribidi from the text). - renderBlockElement( alt_context, node, 0, 0, m_pbuffer->width, m_specified_para_dir ); + // We provide 0,0 as the usable left/right overflows, so no glyph/hanging + // punctuation will leak outside the floatBox. + renderBlockElement( alt_context, node, 0, 0, m_pbuffer->width, 0, 0, m_specified_para_dir ); // (renderBlockElement will ensure style->height if requested.) // Gather footnotes links accumulated by alt_context // (We only need to gather links in the rendering phase, for @@ -640,6 +646,17 @@ class LVFormatter { flt->height = height; flt->to_position = true; + if ( node->getChildCount() > 0 ) { + // The margins were used to position the original + // float node in its wrapping floatBox - so get it + // back from their relative positions + RenderRectAccessor cfmt(node->getChildNode(0)); + if ( flt->is_right ) + flt->inward_margin = cfmt.getX(); + else + flt->inward_margin = width - (cfmt.getX() + cfmt.getWidth()); + } + // If there are already floats to position, don't position any more for now if ( !m_has_float_to_position ) { if ( getNextFloatMinY(flt->clear) == m_y ) { @@ -786,6 +803,16 @@ class LVFormatter { int w = getAvailableWidthAtY(m_y, m_pbuffer->strut_height, x); return w < m_pbuffer->width; } + bool isCurrentLineWithFloatOnLeft() { + int x; + getAvailableWidthAtY(m_y, m_pbuffer->strut_height, x); + return x > 0; + } + bool isCurrentLineWithFloatOnRight() { + int x; + int w = getAvailableWidthAtY(m_y, m_pbuffer->strut_height, x); + return x + w < m_pbuffer->width; + } void checkOngoingFloat() { // Check if there is still some float spanning at current m_y // If there is, next added line will ensure no page split @@ -803,6 +830,65 @@ class LVFormatter { // no need to avoid page split by next line } } + // We prefer to not use the fully usable left overflow, but keep + // a bit of the margin it comes from + #define USABLE_OVERFLOW_USABLE_RATIO 0.8 + // Use this for testing computations and get visually perfect fitting + // #define USABLE_OVERFLOW_USABLE_RATIO 1 + void getCurrentLineUsableOverflows( int & usable_left_overflow, int & usable_right_overflow ) { + if (m_pbuffer->floatcount > 0) { + // We have left or right floats on this line, that might + // make m_usable_left/right_overflow no more relevant. + // We'll allow the main text to overflow in these floats' + // inward margin (the float element content itself is also + // allowed to overflow in it, so its margin is shared; + // hopefully, both overflowing in it at the same position + // will be rare). + // Note that if the float that sets the text min or max x + // have some large inward margin, an other further float + // with less inward margin might be the one that should + // limit the usable overflow. + int fl_left_max_x = 0; + int fl_left_max_x_overflow = - m_usable_left_overflow; + int fl_right_min_x = m_pbuffer->width; + int fl_right_min_x_overflow = m_pbuffer->width + m_usable_right_overflow; + // We need to scan pixel line by pixel line along the strut height to be sure + int y = m_y; + int end_y = y + m_pbuffer->strut_height; + while (y <= end_y) { + for (int i=0; ifloatcount; i++) { + embedded_float_t * flt = m_pbuffer->floats[i]; + if (flt->to_position) // ignore not yet positionned floats + continue; + if (flt->y <= y && flt->y + flt->height > y) { // this float is spanning this y + if (flt->is_right) { + if (flt->x < fl_right_min_x) + fl_right_min_x = flt->x; + if (flt->x + flt->inward_margin < fl_right_min_x_overflow) + fl_right_min_x_overflow = flt->x + flt->inward_margin; + // (inward_margin is the left margin of a right float) + } + else { + if (flt->x + flt->width > fl_left_max_x) + fl_left_max_x = flt->x + flt->width; + if (flt->x + flt->width - flt->inward_margin > fl_left_max_x_overflow) + fl_left_max_x_overflow = flt->x + flt->width - flt->inward_margin; + // (inward_margin is the right margin of a left float) + } + } + } + y += 1; + } + usable_left_overflow = fl_left_max_x - fl_left_max_x_overflow; + usable_right_overflow = fl_right_min_x_overflow - fl_right_min_x; + } + else { + usable_left_overflow = m_usable_left_overflow; + usable_right_overflow = m_usable_right_overflow; + } + usable_left_overflow = usable_left_overflow * USABLE_OVERFLOW_USABLE_RATIO; + usable_right_overflow = usable_right_overflow * USABLE_OVERFLOW_USABLE_RATIO; + } /// allocate buffers for paragraph void allocate( int start, int end ) @@ -1565,44 +1651,6 @@ class LVFormatter { } } - /// checks whether to add more space after italic character - /// (this could be used to shift some regular font glyphs - /// like 'f' that often overflows the glyph - but we let - /// such glyphs overflow in the padding/margin, as it is - /// quite small and possibly intended by the font designer; - /// italic overflows are often larger, and need to be - /// corrected at end of line or end of italic node) - int getAdditionalCharWidth( int pos, int maxpos ) { - if (m_text[pos]==0) // object - return 0; // no additional space - LVFont * font = (LVFont*)m_srcs[pos]->t.font; - if (!font) - return 0; // no font - if ( posgetRightSideBearing(m_text[pos], true, true); - // if (glyph_overflow > 0) printf("right overflow: %c %d\n", m_text[pos], glyph_overflow); - return glyph_overflow; - } - - /// checks whether to add more space on left before italic character - /// (this could be used to shift some regular font glyphs - /// like 'J' whose foot often overflows the glyph - but we let - /// such glyphs overflow in the padding/margin, as it is - /// quite small and possibly intended by the font designer; - /// italic underflows and overflows are often larger, and need - /// to be corrected at start of line) - int getAdditionalCharWidthOnLeft( int pos ) { - if (m_text[pos]==0) // object - return 0; // no additional space - LVFont * font = (LVFont*)m_srcs[pos]->t.font; - // Correct italic_only, including removal of positive leading space, - int glyph_overflow = - font->getLeftSideBearing(m_text[pos], false, true); - // if (glyph_overflow != 0) printf("left overflow %c: %d\n", m_text[pos], glyph_overflow); - return glyph_overflow; - } - /// measure word bool measureWord(formatted_word_t * word, int & width) { @@ -1792,7 +1840,7 @@ class LVFormatter { if ( chars_measured w=%d\n", m_widths[start + k]); } + /* If the following was ever needed, it was wrong to do it at this step + * of measureText(), as we then get additional fixed spacing that we may + * not need in some contexts. So don't do it: browsers do not. + * We'll handle that if LTEXT_FIT_GLYPHS when positionning words + * (not implemented for now.) + + // This checks whether we're the last char of a text node, and if + // this node is italic, it adds the glyph italic overflow to the + // last char width. + // This might not be needed if the next text node is also italic, + // or if there is a space at start of next text node, and it might + // be needed at start of node too as the italic can overflow there too. + // It might also confuse our adjustment at start or end of line. int dw = getAdditionalCharWidth(i-1, m_length); if ( lastDirection < 0 ) // ignore it for RTL (as right side bearing is measured) dw = 0; @@ -1893,12 +1954,9 @@ class LVFormatter { m_widths[i-1] += dw; lastWidth += dw; } + */ lastWidth += widths[len-1]; //lenwidth, m_specified_para_dir, &baseline ); + // We provide 0,0 as the usable left/right overflows, so no glyph/hanging + // punctuation will leak outside the inlineBox (we might provide the widths + // of any blank space on either side, but here is too early as it might be + // shuffled by BiDi reordering.) + renderBlockElement( alt_context, node, 0, 0, m_pbuffer->width, 0, 0, m_specified_para_dir, &baseline ); // (renderBlockElement will ensure style->height if requested.) // Note: this inline box we just rendered can have some overflow @@ -2283,19 +2345,42 @@ class LVFormatter { } /// split line into words, add space for width alignment - void addLine( int start, int end, int x, src_text_fragment_t * para, bool first, bool last, bool preFormattedOnly, bool isLastPara ) + void addLine( int start, int end, int x, src_text_fragment_t * para, bool first, bool last, bool preFormattedOnly, bool isLastPara, bool hasInlineBoxes ) { - int maxWidth = getCurrentLineWidth(); - // provided x is left indent + // No need to do some x-alignment work if light formatting, when we + // are only interested in computing block height and positionning + // floats: 'is_reusable' will be unset, and any attempt at reusing + // this formatting for drawing will cause a non-light re-formatting. + // Except when there are inlineBoxes in the text: we need to correctly + // position them to have their x/y saved in their RenderRectAccessor + // (so getRect() can work accurately before the page is drawn). + bool light_formatting = m_pbuffer->light_formatting && !hasInlineBoxes; + + // todo: we can avoid some more work below when light_formatting (and + // possibly the BiDi re-ordering we need for ordering footnotes, as + // if we don't re-order, we'll always have them in the logical order, + // and we can just append them in lvrend.cpp instead of checking + // where to insert them if RTL - but we'd still have to do that + // if some inlinebox prevent doing light formatting :(.) + + // int maxWidth = getCurrentLineWidth(); // if needed for debug printf() below + + // Provided x is the line indent: as we're making words in the visual + // order here, it will be line start x for LTR paragraphs; but for RTL + // ones, we'll handle it as some reserved space on the right. int rightIndent = 0; if ( m_para_dir_is_rtl ) { rightIndent = x; - maxWidth -= x; // put x/first char indent on the right: reduce width + // maxWidth -= x; // put x/first char indent on the right: reduce width x = getCurrentLineX(); // use shift induced by left floats } else { x += getCurrentLineX(); // add shift induced by left floats } + // Get overflows, needed to position first and last words + int usable_left_overflow; + int usable_right_overflow; + getCurrentLineUsableOverflows(usable_left_overflow, usable_right_overflow); // Find out text alignment to ensure for this line int align = para->flags & LTEXT_FLAG_NEWLINE; @@ -2335,10 +2420,6 @@ class LVFormatter { TR("addLine(%d, %d) y=%d align=%d", start, end, m_y, align); // printf("addLine(%d, %d) y=%d align=%d maxWidth=%d\n", start, end, m_y, align, maxWidth); - // Note: in the code and comments, all these mean the same thing: - // visual alignment enabled, floating punctuation, hanging punctuation - bool visualAlignmentEnabled = (gFlgFloatingPunctuationEnabled != 0) && (align != LTEXT_ALIGN_CENTER); - // Note: parameter needReduceSpace and variable splitBySpaces (which // was always true) have been removed, as we always split by space: // even if we end up not changing spaces' widths, we need to make @@ -2598,13 +2679,12 @@ class LVFormatter { // Some words vertical-align positionning might need to be fixed // only once the whole line has been laid out bool delayed_valign_computation = false; - // alignLine() will have more work to do if we have inlineBox elements - bool has_inline_boxes = false; // Make out words, making a new one when some properties change int wstart = start; - bool lastIsSpace = false; + bool firstWord = true; bool lastWord = false; + bool lastIsSpace = false; bool isSpace = false; bool space = false; // Bidi @@ -2693,7 +2773,7 @@ class LVFormatter { // Note: a "word" in our current context is just a unit of text that // should be rendered together, and can be moved on the x-axis for // alignment purpose (the 2 french words "qu'autrefois" make a - // single "word" here, the single word "quelconque", if hyphentaded + // single "word" here, the single word "quelconque", if hyphenated // as "quel-conque" will make one "word" on this line and another // "word" on the next line. // @@ -2763,7 +2843,7 @@ class LVFormatter { // block cut by
): most browsers don't display the line break // implied by the BR when we have: "
some text
more text" // or "
some text
more text". - if (lastWord && frmline->word_count == 0) { + if (lastWord && firstWord) { if (!isLastPara) { wstart--; // make a single word with a single collapsed space if (m_flags[wstart] & LCHAR_IS_TO_IGNORE) { @@ -2820,7 +2900,6 @@ class LVFormatter { word->min_width = word->width; word->o.height = srcline->o.height; if ( srcline->flags & LTEXT_SRC_IS_INLINE_BOX ) { // inline-block - has_inline_boxes = true; word->flags = LTEXT_WORD_IS_INLINE_BOX; // For inline-block boxes, the baseline may not be the bottom; it has // been computed in measureText(). @@ -2974,14 +3053,6 @@ class LVFormatter { // printf("baseline_to_bottom=%d top_to_baseline=%d word->y=%d txt=|%s|\n", baseline_to_bottom, // top_to_baseline, word->y, UnicodeToLocal(lString16(srcline->t.text, srcline->t.len)).c_str()); - // For Harfbuzz, which may shape differently words at start or end of paragraph - if (first && frmline->word_count == 1) // first line of paragraph + first word of line - word->flags |= LTEXT_WORD_BEGINS_PARAGRAPH; - if (last && lastWord) // last line of paragraph + last word of line - word->flags |= LTEXT_WORD_ENDS_PARAGRAPH; - if ( trustDirection) - word->flags |= LTEXT_WORD_DIRECTION_KNOWN; - // Set word start and end (start+len-1) indices in the source text node if ( !m_has_bidi ) { // No bidi, everything is linear @@ -3019,6 +3090,29 @@ class LVFormatter { word->t.len = m_charindex[i-1] + 1 - m_charindex[wstart]; } + // Flag word that are the start of a link (for in-page footnotes) + if ( word->t.start==0 && srcline->flags & LTEXT_IS_LINK ) { + word->flags |= LTEXT_WORD_IS_LINK_START; + // todo: we might miss some links if the source text starts with a space + } + + // Below this are stuff that could be skipped if light_formatting + // (We need bidi and the above adjustment only to get correctly ordered + // in-page footnotes links.) + + // For Harfbuzz, which may shape differently words at start or end of paragraph. + // todo: this is probably wrong if some multi bidi levels re-ordering has been done + if ( first ) { // first line of paragraph + if ( m_para_dir_is_rtl ? lastWord : firstWord ) + word->flags |= LTEXT_WORD_BEGINS_PARAGRAPH; + } + if ( last ) { // last line of paragraph + if ( m_para_dir_is_rtl ? firstWord : lastWord ) + word->flags |= LTEXT_WORD_ENDS_PARAGRAPH; + } + if ( trustDirection) + word->flags |= LTEXT_WORD_DIRECTION_KNOWN; + // We need to compute how many glyphs can have letter_spacing added, that // might be done in alignLine() (or not). We have to do it now even if // not used, as we won't have that information anymore in alignLine(). @@ -3050,6 +3144,83 @@ class LVFormatter { word->distinct_glyphs += tailing_spaces; } + // If we're asked to fit glyphs (avoid glyphs from overflowing line edges and + // on neighbour text nodes), we might need to tweak words x and width + bool fit_glyphs = srcline->flags & LTEXT_FIT_GLYPHS; + + if ( firstWord && (align == LTEXT_ALIGN_LEFT || align == LTEXT_ALIGN_WIDTH) ) { + // Adjust line start x if needed + // No need to do it when line is centered or right aligned (doing so + // might increase the line width and change space widths for no reason). + // We currently have no chance to get an added hyphen for hyphenation + // at start of line, as we handle only hyphenation with LTR text. + // It feels we have to do it even for the first line with text-indent, + // as some page might have multiple consecutive single lines that can + // benefit from hanging so the margin looks clean too. + int lsb = font->getLeftSideBearing(m_text[wstart]); + int left_overflow = lsb < 0 ? -lsb : 0; + if ( fit_glyphs ) { + // We don't want any part of the glyph to overflow in the left margin. + // We correct only overflows - keeping underflows (so, not having + // the glyph blackbox really fit the edge) respects the natural + // alignment. + // We also prevent hanging punctuation as it de facto overflows. + // (We used to correct it only for italic fonts, where "J" or "f" + // can have have huge negative overflow for their part below baseline + // and so leak on the left. On the left, we were also correcting + // underflows, so fitting italic glyphs to the left edge - but we + // don't anymore as it doesn't really feel needed.) + frmline->x += left_overflow; // so that the glyph's overflow is at original frmline->x + // printf("%c lsb=%d\n", m_text[wstart], font->getLeftSideBearing(m_text[wstart])); + } + else { + // We prevent hanging punctuation on the common opening quotation marks + // or dashes that we flagged with LCHAR_LOCKED_SPACING (most of these + // are characters that can hang) - and on fully-pre lines and when + // the font is monospace. + // Note that some CJK fonts might have full-width glyphs for some of our + // common hanging chars, but not for others, and this might look bad with + // them, and different whether it is used as the main font or as a fallback. + // (Noto Sans CJK SC has full-width glyphs for single or double quotation + // marks (‘ ’ “ ”), but not for all our other hanging chars.) + // Reducing CJK half-blank full-width glyphs's width should be handled + // more generically elsewhere. + // We try to avoid hanging these with some heuristic below. + bool allow_hanging = m_hanging_punctuation && + !preFormattedOnly && + !(m_flags[wstart] & LCHAR_LOCKED_SPACING) && + font->getFontFamily() != css_ff_monospace; + int shift_x = 0; + if ( allow_hanging ) { + bool check_font; + int percent = srcline->lang_cfg->getHangingPercent(false, check_font, m_text, wstart, end-wstart-1); + if ( percent && check_font && left_overflow > 0 ) { + // Some fonts might already have enough negative + // left side bearing for some chars, that would + // make them naturally hang on the left. + percent = 0; + } + if ( percent ) { + int first_char_width = m_widths[wstart] - (wstart>0 ? m_widths[wstart-1] : 0); + shift_x = first_char_width * percent / 100; + if ( shift_x == 0 ) // Force at least 1px if division rounded it to 0 + shift_x = 1; + // Cancel it if this char looks like it might be full-width + // (0.9 * font size, in case HarfBuzz has reduced the advance) + // and it has a lot of positive left side bearing (left half + // of the glyph blank) - see above. + if ( first_char_width > 0.9 * font->getSize() && lsb > 0.4 * first_char_width ) { + shift_x = 0; + } + } + } + if ( shift_x - lsb > usable_left_overflow ) { + shift_x = usable_left_overflow + lsb; + } + frmline->x -= shift_x; + } + } + // Word x position on line: for now, we just stack words after each other. // They will be adjusted if needed in alignLine() word->x = frmline->width; @@ -3110,7 +3281,7 @@ class LVFormatter { word->min_width = word->width; } } - else if ( frmline->word_count>1 && m_flags[wstart] & LCHAR_IS_SPACE ) { + else if ( !firstWord && m_flags[wstart] & LCHAR_IS_SPACE ) { // Current word starts with a space (looks like this should not happen): // we can increase the space between previous word and this one if needed //if ( word->t.len<2 || m_text[i-1]!=UNICODE_NO_BREAK_SPACE || m_text[i-2]!=UNICODE_NO_BREAK_SPACE) @@ -3119,17 +3290,99 @@ class LVFormatter { //else frmline->words[frmline->word_count-2].flags |= LTEXT_WORD_CAN_ADD_SPACE_AFTER; } - else if (frmline->word_count>1 && isCJKIdeograph(m_text[i])) { + else if ( !firstWord && isCJKIdeograph(m_text[i]) ) { // Current word is a CJK char: we can increase the space // between previous word and this one if needed frmline->words[frmline->word_count-2].flags |= LTEXT_WORD_CAN_ADD_SPACE_AFTER; } - // if ( m_flags[i-1] & LCHAR_ALLOW_WRAP_AFTER ) // word->flags |= LTEXT_WORD_CAN_BREAK_LINE_AFTER; // not used anywhere - if ( word->t.start==0 && srcline->flags & LTEXT_IS_LINK ) - word->flags |= LTEXT_WORD_IS_LINK_START; // for in-page footnotes + if ( lastWord && (align == LTEXT_ALIGN_RIGHT || align == LTEXT_ALIGN_WIDTH) ) { + // Adjust line end if needed. + // If we need to adjust last word's last char, we need to put the delta + // in this word->width, which will make it into frmline->width. + + // Find the real last drawn glyph + int lastnonspace = i-1; + for ( int k=i-1; k>=wstart; k-- ) { + if ( !(m_flags[k] & LCHAR_IS_SPACE) ) { + lastnonspace = k; + break; + } + } + bool ends_with_hyphen = m_flags[lastnonspace] & LCHAR_ALLOW_HYPH_WRAP_AFTER; + int rsb = 0; // don't bother with hyphen rsb, which can't overflow + int right_overflow = 0; + if ( !ends_with_hyphen ) { + rsb = font->getRightSideBearing(m_text[lastnonspace]); + if ( rsb < 0 ) + right_overflow = -rsb; + } + if ( fit_glyphs ) { + // We don't want any part of the glyph to overflow in the right margin. + // (We used to correct it only for italic fonts, where "J" or "f" + // can have have huge negative overflow for their part above baseline + // and so leak on the right. We were previously also correcting only + // overflows and not underflows.) + word->width += right_overflow; + } + else { + // We prevent hanging punctuation in a few cases (see above) + bool allow_hanging = m_hanging_punctuation && + !preFormattedOnly && + font->getFontFamily() != css_ff_monospace; + int shift_w = 0; + if ( allow_hanging ) { + if ( ends_with_hyphen ) { + int percent = srcline->lang_cfg->getHyphenHangingPercent(); + if ( percent ) { + shift_w = font->getHyphenWidth() * percent / 100; + if ( shift_w == 0 ) // Force at least 1px if division rounded it to 0 + shift_w = 1; + } + // Note: some part of text in bold or in a bigger font size inside + // a paragraph may stand out more than the regular text, and this + // is quite noticable with the hyphen. + // We might want to limit or force hyphen hanging to what it should + // be with the main paragraph font, but that might not work well in + // some situations. + // See https://github.com/koreader/crengine/pull/355#issuecomment-656760791 + } + else { + bool check_font; + int percent = srcline->lang_cfg->getHangingPercent(true, check_font, m_text, lastnonspace, end-lastnonspace-1); + if ( percent && check_font && right_overflow > 0 ) { + // Some fonts might already have enough negative + // right side bearing for some chars, that would + // make them naturally hang on the right. + percent = 0; + } + if ( percent ) { + int last_char_width = m_widths[lastnonspace] - (lastnonspace>0 ? m_widths[lastnonspace-1] : 0); + shift_w = last_char_width * percent / 100; + if ( shift_w == 0 ) // Force at least 1px if division rounded it to 0 + shift_w = 1; + // Cancel it if this char looks like it might be full-width + // (0.9 * font size, in case HarfBuzz has reduced the advance) + // and it has a lot of positive right side bearing (right half + // of the glyph blank) - see comment above in 'firstWord' handling. + if ( last_char_width > 0.9 * font->getSize() && rsb > 0.4 * last_char_width ) { + shift_w = 0; + } + } + } + } + if ( shift_w - rsb > usable_right_overflow ) { + shift_w = usable_right_overflow + rsb; + } + word->width -= shift_w; + } + } + + /* Hanging punctuation (with CJK specifics) old code: + * + bool visualAlignmentEnabled = m_hanging_punctuation && (align != LTEXT_ALIGN_CENTER); if ( visualAlignmentEnabled && lastWord ) { // if floating punctuation enabled int endp = i-1; int lastc = m_text[endp]; @@ -3224,6 +3477,8 @@ class LVFormatter { } word->min_width = word->width; } // done if floating punctuation enabled + * End of old code for handling hanging punctuation + */ // printf("addLine - word(%d, %d) x=%d (%d..%d)[%d>%d %x] |%s|\n", wstart, i, // frmline->width, wstart>0 ? m_widths[wstart-1] : 0, m_widths[i-1], word->width, @@ -3284,6 +3539,7 @@ class LVFormatter { } frmline->width += word->width; + firstWord = false; lastSrc = newSrc; wstart = i; @@ -3346,16 +3602,9 @@ class LVFormatter { } } - // Fix up words position and width to ensure requested alignment and indent - // No need to do that if light formatting, as this won't affect the - // block height and floats positionning - is_reusable will be unset, - // and any attempt at reusing this formatting for drawing will cause - // a non-light re-formatting. Except when there are inlineBoxes in the - // text: we need to correctly position them to have their x/y saved - // in their RenderRectAccessor (so getRect() can work accurately before - // the page is drawn). - if ( !m_pbuffer->light_formatting || has_inline_boxes ) { - alignLine( frmline, align, rightIndent, has_inline_boxes ); + if ( !light_formatting ) { + // Fix up words position and width to ensure requested alignment and indent + alignLine( frmline, align, rightIndent, hasInlineBoxes ); } // Get ready for next line @@ -3396,6 +3645,7 @@ class LVFormatter { c >= UNICODE_CJK_PUNCTUATION_HALF_AND_FULL_WIDTH_END ); } + #if (USE_LIBUNIBREAK!=1) bool isCJKPunctuation(lChar16 c) { return ( c >= UNICODE_CJK_PUNCTUATION_BEGIN && c <= UNICODE_CJK_PUNCTUATION_END ) || ( c >= UNICODE_GENERAL_PUNCTUATION_BEGIN && c <= UNICODE_GENERAL_PUNCTUATION_END && @@ -3413,6 +3663,7 @@ class LVFormatter { c==0x3008 || c==0x300a || c==0x300c || c==0x300e || c==0x3010 || // 〈 《 「 『 【 CJK left brackets c==0xff08; // ( fullwidth left parenthesis } + #endif bool isLeftPunctuation(lChar16 c) { // Opening quotation marks and dashes that we don't want a followup space to @@ -3460,28 +3711,6 @@ class LVFormatter { preFormattedOnly = preFormattedOnly && lfFound; } - bool visualAlignmentEnabled = gFlgFloatingPunctuationEnabled!=0; - int visualAlignmentWidth = 0; - if ( visualAlignmentEnabled ) { - // We remove from the available width the max of the max width - // of -/./,/!/? (and other CJK ones) in all fonts used in that - // paragraph, to reserve room for it in case we get one hanging. - // (This will lead to messy variable paragraph widths if some - // paragraph use some bigger font for some inline parts, and - // others don't.) - LVFont * font = NULL; - for ( int i=start; isrctext[i].flags & LTEXT_SRC_IS_OBJECT) ) { - font = (LVFont*)m_pbuffer->srctext[i].t.font; - if (font) { - int dx = font->getVisualAligmentWidth(); - if ( dx>visualAlignmentWidth ) - visualAlignmentWidth = dx; - } - } - } - } - // Not per-specs, but when floats reduce the available width, skip y until // we have the width to draw at least a few chars on a line. // We use N x strut_height because it's one easily acccessible font metric here. @@ -3493,6 +3722,14 @@ class LVFormatter { int upSkipPos = -1; #endif + // Note: we no longer adjust here x and width to account for first or + // last italic glyphs side bearings or hanging punctuation, as here, + // we're still just walking the text in logical order, which might + // be re-ordered when BiDi. + // We'll handle that in AddLine() where we'll make words in visual + // order; the small shifts we might have on the final width vs the + // width measured here will hopefully be compensated on the space chars. + while ( posflags & LTEXT_SRC_IS_FLOAT_DONE) ) { - int currentWidth = x + firstCharMargin + m_widths[i]-w0 - spaceReduceWidth; - addFloat( src, currentWidth ); - src->flags |= LTEXT_SRC_IS_FLOAT_DONE; - maxWidth = getCurrentLineWidth(); + if ( flags & LCHAR_IS_OBJECT ) { + if ( m_charindex[i] == FLOAT_CHAR_INDEX ) { // float + src_text_fragment_t * src = m_srcs[i]; + // Not sure if we can be called again on the same LVFormatter + // object, but the whole code allows for re-formatting and + // they should give the same result. + // So, use a flag to not re-add already processed floats. + if ( !(src->flags & LTEXT_SRC_IS_FLOAT_DONE) ) { + int currentWidth = x + m_widths[i]-w0 - spaceReduceWidth; + addFloat( src, currentWidth ); + src->flags |= LTEXT_SRC_IS_FLOAT_DONE; + maxWidth = getCurrentLineWidth(); + } + // We don't set lastNormalWrap when collapsed spaces, + // so let's not for floats either. + // But we need to when the float is the last source (as + // done below, otherwise we would not update wrapPos and + // we'd get another ghost line, and this real last line + // might be wrongly justified). + if ( i==m_length-1 ) { + lastNormalWrap = i; + } + continue; } - // We don't set lastNormalWrap when collapsed spaces, - // so let's not for floats either. - // But we need to when the float is the last source (as - // done below, otherwise we would not update wrapPos and - // we'd get another ghost line, and this real last line - // might be wrongly justified). - if ( i==m_length-1 ) { - lastNormalWrap = i; + if ( m_charindex[i] == INLINEBOX_CHAR_INDEX && firstInlineBoxPos < 0 ) { + firstInlineBoxPos = i; } - continue; } // We would not need to bother with LCHAR_IS_COLLAPSED_SPACE, as they have zero // width and so can be grabbed here. They carry LCHAR_ALLOW_WRAP_AFTER just like @@ -3587,39 +3827,10 @@ class LVFormatter { fillAndMoveToY( new_y ); maxWidth = getCurrentLineWidth(); } - // Shift first italic glyph whose part below baseline might leak on the left. - // Note: we might not need to bother with negative left side bearing, as we - // now can have them in the margin as we don't clip anymore. So, we could - // just have italic "J" or "f" drawn a bit in the margin. - // But shifting them that way makes for nicer multilines higlighting boxes. - firstCharMargin = getAdditionalCharWidthOnLeft(pos); - if ( visualAlignmentEnabled ) { // Floating punctuation - maxWidth -= visualAlignmentWidth; - spaceReduceWidth -= visualAlignmentWidth/2; - firstCharMargin += visualAlignmentWidth/2; - if (isCJKLeftPunctuation(m_text[pos])) { - // Make that left punctuation left-hanging by reducing firstCharMargin - LVFont * fnt = (LVFont *)m_srcs[pos]->t.font; - if (fnt) - firstCharMargin -= fnt->getCharWidth(m_text[pos]); - firstCharMargin = (x + firstCharMargin) > 0 ? firstCharMargin : 0; - } - } - if ( m_has_bidi ) { - // If bidi, our first char may be no more the first char - // inside AddLine, so reset firtCharMargin to 0. - // (a bit sad to do that if there's a single RTL word - // in the middle of line...) - firstCharMargin = 0; - // todo: probably some other things to avoid if bidi or - // if m_para_dir_is_rtl, like hyphenation. - // Also possible: scan chars as they fit on this line for - // bidi level > 1: if none, this line is pure LTR - } } bool grabbedExceedingSpace = false; - if ( x + firstCharMargin + m_widths[i]-w0 > maxWidth + spaceReduceWidth ) { + if ( x + m_widths[i]-w0 > maxWidth + spaceReduceWidth ) { // It's possible the char at i is a space whose width exceeds maxWidth, // but it should be a candidate for lastNormalWrap (otherwise, the // previous word will be hyphenated and we will get spaces widen for @@ -3703,11 +3914,12 @@ class LVFormatter { m_pbuffer->min_space_condensing_percent != 100 && i < m_length-1 && ( m_flags[i] & LCHAR_IS_SPACE ) && - ( i==m_length-1 || !(m_flags[i + 1] & LCHAR_IS_SPACE) ) ) { + !(m_flags[i+1] & LCHAR_IS_SPACE) ) { // Each space not followed by a space is candidate for space condensing int dw = getMaxCondensedSpaceTruncation(i); if ( dw>0 ) spaceReduceWidth += dw; + // TODO do that too for CJK punctuation whose glyph might be half blank } if (grabbedExceedingSpace) break; // delayed break @@ -3735,9 +3947,6 @@ class LVFormatter { #endif int normalWrapWidth = lastNormalWrap > 0 ? x + m_widths[lastNormalWrap]-w0 : 0; int unusedSpace = maxWidth - normalWrapWidth; - if ( visualAlignmentEnabled ) { - unusedSpace -= 2*visualAlignmentWidth; - } int unusedPercent = maxWidth > 0 ? unusedSpace * 100 / maxWidth : 0; #if (USE_LIBUNIBREAK!=1) // (Different usage of deprecatedWrap than above) @@ -3825,7 +4034,7 @@ class LVFormatter { for ( int i=0; i 0 && !(m_flags[endp-1] & LCHAR_ALLOW_HYPH_WRAP_AFTER) ) { - // Find the real last displayed glyph, skipping spaces and floats - int lastnonspace = endp-1; - for ( int k=endp-1; k>=start; k-- ) { - if ( !(m_flags[k] & LCHAR_IS_SPACE) && - !( (m_flags[k] & LCHAR_IS_OBJECT) && (m_charindex[k] == FLOAT_CHAR_INDEX) ) ) { - lastnonspace = k; - break; - } - } - // If the last non-space/non-float is an image or an inline-block box, we don't do it. - // Note: it feels we should do that for the char before ANY image on the line (so the italic - // glyph does not overlap with the image). It's unclear whether the former code did that - // (or not) for the char before an image at end of line only... - if ( !(m_flags[lastnonspace] & LCHAR_IS_OBJECT) ) { - // todo: probably need be avoided if bidi/rtl: - int dw = lastnonspace>=start ? getAdditionalCharWidth(lastnonspace, lastnonspace+1) : 0; - if (dw) { - TR("additional width = %d, after char %s", dw, LCSTR(lString16(m_text + lastnonspace, 1))); - m_widths[lastnonspace] += dw; - } - } - } - - addLine(pos, endp, x + firstCharMargin, para, pos==0, wrapPos>=m_length-1, preFormattedOnly, isLastPara); + bool hasInlineBoxes = firstInlineBoxPos >= 0 && firstInlineBoxPos < endp; + addLine(pos, endp, x, para, pos==0, wrapPos>=m_length-1, preFormattedOnly, isLastPara, hasInlineBoxes); pos = wrapPos + 1; // start of next line #if (USE_LIBUNIBREAK==1) @@ -4030,7 +4216,13 @@ class LVFormatter { // that, and we could get some small mismatches and glitches. rend_flags &= ~BLOCK_RENDERING_ALLOW_NEGATIVE_COLLAPSED_MARGINS; int baseline = REQ_BASELINE_FOR_TABLE; // baseline of block is baseline of its first line - renderBlockElement( context, node, 0, 0, width, m_specified_para_dir, &baseline, rend_flags); + // The same usable overflows provided for the container (possibly + // adjusted for floats) can be used for this full-width inlineBox. + int usable_left_overflow; + int usable_right_overflow; + getCurrentLineUsableOverflows(usable_left_overflow, usable_right_overflow); + renderBlockElement( context, node, 0, 0, width, usable_left_overflow, usable_right_overflow, + m_specified_para_dir, &baseline, rend_flags); RenderRectAccessor fmt( node ); fmt.setX(block_x); fmt.setY(m_y); @@ -4223,7 +4415,9 @@ static void freeFrmLines( formatted_text_fragment_t * m_pbuffer ) } // experimental formatter -lUInt32 LFormattedText::Format(lUInt16 width, lUInt16 page_height, int para_direction, BlockFloatFootprint * float_footprint) +lUInt32 LFormattedText::Format(lUInt16 width, lUInt16 page_height, int para_direction, + int usable_left_overflow, int usable_right_overflow, bool hanging_punctuation, + BlockFloatFootprint * float_footprint) { // clear existing formatted data, if any freeFrmLines( m_pbuffer ); @@ -4255,6 +4449,10 @@ lUInt32 LFormattedText::Format(lUInt16 width, lUInt16 page_height, int para_dire // it will be detected by fribidi) formatter.m_specified_para_dir = para_direction; + formatter.m_usable_left_overflow = usable_left_overflow; + formatter.m_usable_right_overflow = usable_right_overflow; + formatter.m_hanging_punctuation = hanging_punctuation; + if (float_footprint) { formatter.m_no_clear_own_floats = float_footprint->no_clear_own_floats; @@ -4273,6 +4471,7 @@ lUInt32 LFormattedText::Format(lUInt16 width, lUInt16 page_height, int para_dire flt->width = float_footprint->floats[i][2]; flt->height = float_footprint->floats[i][3]; flt->is_right = (bool)(float_footprint->floats[i][4]); + flt->inward_margin = float_footprint->floats[i][5]; } } diff --git a/crengine/src/lvtinydom.cpp b/crengine/src/lvtinydom.cpp index c9b3647e0..d96b53375 100644 --- a/crengine/src/lvtinydom.cpp +++ b/crengine/src/lvtinydom.cpp @@ -84,7 +84,7 @@ int gDOMVersionRequested = DOM_VERSION_CURRENT; /// change in case of incompatible changes in swap/cache file format to avoid using incompatible swap file // increment to force complete reload/reparsing of old file -#define CACHE_FILE_FORMAT_VERSION "3.05.44k" +#define CACHE_FILE_FORMAT_VERSION "3.05.45k" /// increment following value to force re-formatting of old book after load #define FORMATTING_VERSION_ID 0x0025 @@ -390,8 +390,11 @@ lUInt32 calcGlobalSettingsHash(int documentId, bool already_rendered) // hash = hash * 31 + (int)fontMan->GetHintingMode(); if ( LVRendGetFontEmbolden() ) hash = hash * 75 + 2384761; - if ( gFlgFloatingPunctuationEnabled ) - hash = hash * 75 + 1761; + // Hanging punctuation does not need to trigger a re-render, as + // it's now ensured by alignLine() and won't change paragraphs height. + // We just need to _renderedBlockCache.clear() when it changes. + // if ( gHangingPunctuationEnabled ) + // hash = hash * 75 + 1761; hash = hash * 31 + gRenderDPI; hash = hash * 31 + gRenderBlockRenderingFlags; hash = hash * 31 + gRootFontSize; @@ -1617,6 +1620,58 @@ int RenderRectAccessor::getInnerWidth() } return _inner_width; } +int RenderRectAccessor::getUsableLeftOverflow() +{ + if ( _dirty ) { + _dirty = false; + _node->getRenderData(*this); +#ifdef DEBUG_RENDER_RECT_ACCESS + rr_lock( _node ); +#endif + } + return _usable_left_overflow; +} +int RenderRectAccessor::getUsableRightOverflow() +{ + if ( _dirty ) { + _dirty = false; + _node->getRenderData(*this); +#ifdef DEBUG_RENDER_RECT_ACCESS + rr_lock( _node ); +#endif + } + return _usable_right_overflow; +} +void RenderRectAccessor::setUsableLeftOverflow( int dx ) +{ + if ( _dirty ) { + _dirty = false; + _node->getRenderData(*this); +#ifdef DEBUG_RENDER_RECT_ACCESS + rr_lock( _node ); +#endif + } + if ( dx < 0 ) dx = 0; // don't allow a negative value + if ( _usable_left_overflow != dx ) { + _usable_left_overflow = dx; + _modified = true; + } +} +void RenderRectAccessor::setUsableRightOverflow( int dx ) +{ + if ( _dirty ) { + _dirty = false; + _node->getRenderData(*this); +#ifdef DEBUG_RENDER_RECT_ACCESS + rr_lock( _node ); +#endif + } + if ( dx < 0 ) dx = 0; // don't allow a negative value + if ( _usable_right_overflow != dx ) { + _usable_right_overflow = dx; + _modified = true; + } +} int RenderRectAccessor::getTopOverflow() { if ( _dirty ) { @@ -4519,7 +4574,9 @@ bool ldomDocument::parseStyleSheet(lString16 cssFile) return parser.Parse(cssFile); } -bool ldomDocument::render( LVRendPageList * pages, LVDocViewCallback * callback, int width, int dy, bool showCover, int y0, font_ref_t def_font, int def_interline_space, CRPropRef props ) +bool ldomDocument::render( LVRendPageList * pages, LVDocViewCallback * callback, int width, int dy, + bool showCover, int y0, font_ref_t def_font, int def_interline_space, + CRPropRef props, int usable_left_overflow, int usable_right_overflow ) { CRLog::info("Render is called for width %d, pageHeight=%d, fontFace=%s, docFlags=%d", width, dy, def_font->getTypeFace().c_str(), getDocFlags() ); CRLog::trace("initializing default style..."); @@ -4627,7 +4684,7 @@ bool ldomDocument::render( LVRendPageList * pages, LVDocViewCallback * callback, context.setCallback(callback, numFinalBlocks); //updateStyles(); CRLog::trace("rendering..."); - renderBlockElement( context, getRootNode(), 0, y0, width ); + renderBlockElement( context, getRootNode(), 0, y0, width, usable_left_overflow, usable_right_overflow ); _rendered = true; #if 0 //def _DEBUG LVStreamRef ostream = LVOpenFileStream( "test_save_after_init_rend_method.xml", LVOM_WRITE ); @@ -8784,13 +8841,9 @@ bool ldomXPointer::getRect(lvRect & rect, bool extended, bool adjusted) const int chw = w[ offset - word->t.start ] - chx; bool hyphen_added = false; if ( offset == word->t.start + word->t.len - 1 - && (word->flags & LTEXT_WORD_CAN_HYPH_BREAK_LINE_AFTER) - && !gFlgFloatingPunctuationEnabled ) { + && (word->flags & LTEXT_WORD_CAN_HYPH_BREAK_LINE_AFTER) ) { // if offset is the end of word, and this word has // been hyphenated, includes the hyphen width - // (but not when floating punctuation is enabled, - // to keep nice looking rectangles on multi lines - // text selection) chw += font->getHyphenWidth(); // We then should not account for the right side // bearing below @@ -8955,13 +9008,9 @@ bool ldomXPointer::getRect(lvRect & rect, bool extended, bool adjusted) const int chw = w[ offset - word->t.start ] - chx; bool hyphen_added = false; if ( offset == word->t.start + word->t.len - 1 - && (word->flags & LTEXT_WORD_CAN_HYPH_BREAK_LINE_AFTER) - && !gFlgFloatingPunctuationEnabled ) { + && (word->flags & LTEXT_WORD_CAN_HYPH_BREAK_LINE_AFTER) ) { // if offset is the end of word, and this word has // been hyphenated, includes the hyphen width - // (but not when floating punctuation is enabled, - // to keep nice looking rectangles on multi lines - // text selection) chw += font->getHyphenWidth(); // We then should not account for the right side // bearing below @@ -17146,22 +17195,26 @@ int ldomNode::renderFinalBlock( LFormattedTextRef & frmtext, RenderRectAccessor f = getDocument()->createFormattedText(); if ( rm != erm_final ) return 0; - //RenderRectAccessor fmt( this ); - /// render whole node content as single formatted object + + /// Render whole node content as single formatted object + + // Get some properties cached in this node's RenderRectAccessor + // and set the initial flags and lang_cfg (for/from the final node + // itself) for renderFinalBlock(), int direction = RENDER_RECT_PTR_GET_DIRECTION(fmt); lUInt32 flags = styleToTextFmtFlags( true, getStyle(), 0, direction ); int lang_node_idx = fmt->getLangNodeIndex(); TextLangCfg * lang_cfg = TextLangMan::getTextLangCfg(lang_node_idx>0 ? getDocument()->getTinyNode(lang_node_idx) : NULL); + + // Add this node's inner content (text and children nodes) as source text + // and image fragments into the empty LFormattedText object ::renderFinalBlock( this, f.get(), fmt, flags, 0, -1, lang_cfg ); // We need to store this LFormattedTextRef in the cache for it to // survive when leaving this function (some callers do use it). cache.set( this, f ); - bool flg=gFlgFloatingPunctuationEnabled; - if (this->getNodeName()=="th"||this->getNodeName()=="td"|| - (!this->getParentNode()->isNull()&&this->getParentNode()->getNodeName()=="td")|| - (!this->getParentNode()->isNull()&&this->getParentNode()->getNodeName()=="th")) { - gFlgFloatingPunctuationEnabled=false; - } + + // Gather some outer properties and context, so we can format (render) + // the inner content in that context. // This page_h we provide to f->Format() is only used to enforce a max height to images int page_h = getDocument()->getPageHeight(); // Save or restore outer floats footprint (it is only provided @@ -17183,8 +17236,22 @@ int ldomNode::renderFinalBlock( LFormattedTextRef & frmtext, RenderRectAccessor // one that is on a page to be drawn will be reformatted . f->requestLightFormatting(); } - int h = f->Format((lUInt16)width, (lUInt16)page_h, direction, float_footprint); - gFlgFloatingPunctuationEnabled=flg; + int usable_left_overflow = fmt->getUsableLeftOverflow(); + int usable_right_overflow = fmt->getUsableRightOverflow(); + + // Note: some properties are set into LFormattedText by lvrend.cpp's renderFinalBlock(), + // while some others are only passed below as parameters to LFormattedText->Format(). + // The former should logically be source inner content properties (strut, text indent) + // while the latter should be formatting and outer context properties (block width, + // page height...). + // There might be a few drifts from that logic, or duplicates ('direction' is + // passed both ways), that could need a little rework. + + // Format/render inner content: this makes lines and words, which are + // cached into the LFormattedText and ready to be used for drawing + // and text selection. + int h = f->Format((lUInt16)width, (lUInt16)page_h, direction, usable_left_overflow, usable_right_overflow, + gHangingPunctuationEnabled, float_footprint); frmtext = f; //CRLog::trace("Created new formatted object for node #%08X", (lUInt32)this); return h; diff --git a/crengine/src/textlang.cpp b/crengine/src/textlang.cpp index c5bc9edc6..149e6a023 100644 --- a/crengine/src/textlang.cpp +++ b/crengine/src/textlang.cpp @@ -705,3 +705,183 @@ lString16 & TextLangCfg::getClosingQuote( bool update_level ) { _quote_nesting_level--; return ((_quote_nesting_level+1) % 2) ? _close_quote1 : _close_quote2; } + +int TextLangCfg::getHyphenHangingPercent() { + return 70; // 70% +} + +int TextLangCfg::getHangingPercent( bool right_hanging, bool & check_font, const lChar16 * text, int pos, int next_usable ) { + // We get provided with the BiDi re-ordered m_text (so, visually + // ordered) and the index of char: if needed, we can look at + // previous or next chars for context to decide how much to hang + // (i.e. consecutive punctuations). + + // If we ever need to tweak this per language, try to avoid checks + // for the lang_tag in here: + // - either set bool members to enable or disable some checks and tweaks + // - or make this hanging_percent_func_generic, and add dedicated + // functions per language, hanging_percent_func_french, that + // could fallback to calling hanging_percent_func_generic after + // some checks - and have TextLangCfg::getHangingPercent() call + // the dedicated function pointer stored as a member. + + // We might want to prevent any hanging with Chinese and Japanese + // as the text might be mostly full-width glyphs, and this might + // break the grid. This is less risky if the main font is a CJK + // font, but if it is not, punctuation might be picked from the + // main non-CJK font and won't be full-width. + // Or we could round any value to 0 or 100% (and/or tweak any + // glyph in lvtextfm.cpp so it looks like it is full-width). + + lChar16 ch = text[pos]; + int ratio = 0; + + // In French, there's usually a space before and after guillemets, + // or before a quotation mark. Having them hanging, and then a + // space, looks like there's a hole in the margin. + // So, avoid hanging if the next/prev char is a space char. + // This might not happen in other languages, so let's do that + // prevention generically. If needed, make that dependant on + // a boolean member, set to true if LANG_STARTS_WITH(("fr")). + if ( right_hanging ) { + if ( pos > 0 ) { + lChar16 prev_ch = text[pos-1]; + if ( prev_ch == 0x0020 || prev_ch == 0x00A0 || (prev_ch >= 0x2000 && prev_ch <= 0x200A ) ) { + // Normal space, no-break space, and other unicode spaces (except zero-width ones) + return 0; + } + } + } + else { + if ( next_usable > 0 ) { + lChar16 next_ch = text[pos+1]; + if ( next_ch == 0x0020 || next_ch == 0x00A0 || (next_ch >= 0x2000 && next_ch <= 0x200A ) ) { + // Normal space, no-break space, and other unicode spaces (except zero-width ones) + return 0; + } + } + } + + // For the common punctuations, parens and quotes, we check and + // return the same value whether asked for left or right hanging. + // Normally, libunibreak has prevented them from happening on + // one of the sides - but with RTL text, they may happen on + // the other side. Also, some BiDi mirrorable chars "([])" might + // be mirrored in the provided *text when not-using HarfBuzz, but + // won't be mirrored when using HarfBuzz - so let's handle + // all of them no matter the hanging side asked for. + // Also, because in some languages, quotation marks and guillemets + // are used reverted, we include left and right ones in both sets. + + // Most values taken from the "protusion" section in: + // https://source.contextgarden.net/tex/context/base/mkiv/font-imp-quality.lua + // https://www.w3.org/Mail/flatten/index?subject=Amending+hanging-punctuation+for+Western+typography&list=www-style + // and the microtypography thesis: http://www.pragma-ade.nl/pdftex/thesis.pdf + // (screenshot at https://github.com/koreader/koreader/issues/6235#issuecomment-639307634) + + switch (ch) { + case 0x0027: // ' single quote + case 0x002C: // , comma + case 0x002D: // - minus + case 0x002E: // . period + case 0x0060: // ` back quote + // case 0x00AD: // soft hyphen (we don't draw them, so don't handle them) + case 0x060C: // ، arabic comma + case 0x06D4: // ۔ arabic full stop + case 0x2010: // ‐ hyphen + case 0x2018: // ‘ left single quotation mark + case 0x2019: // ’ right single quotation mark + case 0x201A: // ‚ single low-9 quotation mark + case 0x201B: // ‛ single high-reversed-9 quotation mark + case 0x2039: // ‹ left single guillemet + case 0x203A: // › right single guillemet + ratio = 70; + break; + case 0x0022: // " double quote + case 0x003A: // : colon + case 0x003B: // ; semicolon + case 0x00AB: // « left guillemet + case 0x00BB: // » right guillemet + case 0x061B: // ؛ arabic semicolon + case 0x201C: // “ left double quotation mark + case 0x201D: // ” right double quotation mark + case 0x201E: // „ double low-9 quotation mark + case 0x201F: // ‟ double high-reversed-9 quotation mark + ratio = 50; + break; + case 0x2013: // – endash + ratio = 30; + break; + case 0x0021: // ! + case 0x003F: // ? + case 0x00A1: // ¡ + case 0x00BF: // ¿ + case 0x061F: // ؟ + case 0x2014: // — emdash + case 0x2026: // … ellipsis + ratio = 20; + break; + case 0x0028: // ( + case 0x0029: // ) + case 0x005B: // [ + case 0x005D: // ] + case 0x007B: // { + case 0x007D: // } + ratio = 5; + break; + default: + break; + } + if ( ratio ) { + check_font = false; + return ratio; + } + // Other are non punctuation but slight adjustment for some letters, + // that might be ignored if the font already include some negative + // left side bearing. + check_font = true; + if ( right_hanging ) { + switch (ch) { + case 'A': + case 'F': + case 'K': + case 'L': + case 'T': + case 'V': + case 'W': + case 'X': + case 'Y': + case 'k': + case 'r': + case 't': + case 'v': + case 'w': + case 'x': + case 'y': + ratio = 5; + break; + default: + break; + } + } + else { // left hanging + switch (ch) { + case 'A': + case 'J': + case 'T': + case 'V': + case 'W': + case 'X': + case 'Y': + case 'v': + case 'w': + case 'x': + case 'y': + ratio = 5; + break; + default: + break; + } + } + return ratio; +}