1564 lines
43 KiB
JavaScript
1564 lines
43 KiB
JavaScript
/*
|
|
* (c) Copyright Ascensio System SIA 2010-2024
|
|
*
|
|
* This program is a free software product. You can redistribute it and/or
|
|
* modify it under the terms of the GNU Affero General Public License (AGPL)
|
|
* version 3 as published by the Free Software Foundation. In accordance with
|
|
* Section 7(a) of the GNU AGPL its Section 15 shall be amended to the effect
|
|
* that Ascensio System SIA expressly excludes the warranty of non-infringement
|
|
* of any third-party rights.
|
|
*
|
|
* This program is distributed WITHOUT ANY WARRANTY; without even the implied
|
|
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For
|
|
* details, see the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
|
|
*
|
|
* You can contact Ascensio System SIA at 20A-6 Ernesta Birznieka-Upish
|
|
* street, Riga, Latvia, EU, LV-1050.
|
|
*
|
|
* The interactive user interfaces in modified source and object code versions
|
|
* of the Program must display Appropriate Legal Notices, as required under
|
|
* Section 5 of the GNU AGPL version 3.
|
|
*
|
|
* Pursuant to Section 7(b) of the License you must retain the original Product
|
|
* logo when distributing the program. Pursuant to Section 7(e) we decline to
|
|
* grant you any rights under trademark law for use of our trademarks.
|
|
*
|
|
* All the Product's GUI elements, including illustrations and icon sets, as
|
|
* well as technical writing content are licensed under the terms of the
|
|
* Creative Commons Attribution-ShareAlike 4.0 International. See the License
|
|
* terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
|
|
*
|
|
*/
|
|
|
|
"use strict";
|
|
|
|
(
|
|
/**
|
|
* @param {Window} window
|
|
* @param {undefined} undefined
|
|
*/
|
|
function (window, undefined) {
|
|
|
|
|
|
/*
|
|
* Import
|
|
* -----------------------------------------------------------------------------
|
|
*/
|
|
var asc = window["Asc"];
|
|
var asc_debug = asc.outputDebugStr;
|
|
var asc_typeof = asc.typeOf;
|
|
var asc_round = asc.round;
|
|
|
|
function LineInfo(lm) {
|
|
this.tw = 0;
|
|
this.th = 0;
|
|
this.bl = 0;
|
|
this.a = 0;
|
|
this.d = 0;
|
|
this.beg = undefined;
|
|
this.end = undefined;
|
|
this.startX = undefined;
|
|
|
|
this.assign(lm);
|
|
}
|
|
|
|
LineInfo.prototype.assign = function (lm) {
|
|
if (lm) {
|
|
this.th = lm.th;
|
|
this.bl = lm.bl;
|
|
this.a = lm.a;
|
|
this.d = lm.d;
|
|
}
|
|
};
|
|
|
|
|
|
LineInfo.prototype.initStartX = function (lineWidth, x, maxWidth, align) {
|
|
var x_ = x;
|
|
if (align === AscCommon.align_Right) {
|
|
x_ = x + maxWidth - lineWidth - 1;
|
|
} else if (align === AscCommon.align_Center) {
|
|
x_ = x + 0.5 * (maxWidth - lineWidth);
|
|
}
|
|
this.startX = x_;
|
|
return x_;
|
|
};
|
|
|
|
/** @constructor */
|
|
function lineMetrics() {
|
|
this.th = 0;
|
|
this.bl = 0;
|
|
this.bl2 = 0;
|
|
this.a = 0;
|
|
this.d = 0;
|
|
}
|
|
|
|
lineMetrics.prototype.clone = function () {
|
|
var oRes = new lineMetrics();
|
|
oRes.th = this.th;
|
|
oRes.bl = this.bl;
|
|
oRes.bl2 = this.bl2;
|
|
oRes.a = this.a;
|
|
oRes.d = this.d;
|
|
return oRes;
|
|
};
|
|
|
|
/** @constructor */
|
|
function charProperties() {
|
|
this.grapheme = AscFonts.NO_GRAPHEME;
|
|
this.c = undefined;
|
|
this.lm = undefined;
|
|
this.fm = undefined;
|
|
this.fsz = undefined;
|
|
this.font = undefined;
|
|
this.va = undefined;
|
|
this.nl = undefined;
|
|
this.hp = undefined;
|
|
this.delta = undefined;
|
|
this.skip = undefined;
|
|
this.repeat = undefined;
|
|
this.total = undefined;
|
|
this.wrd = undefined;
|
|
}
|
|
|
|
charProperties.prototype.clone = function () {
|
|
var oRes = new charProperties();
|
|
oRes.grapheme = this.grapheme;
|
|
oRes.c = (undefined !== this.c) ? this.c.clone() : undefined;
|
|
oRes.lm = (undefined !== this.lm) ? this.lm.clone() : undefined;
|
|
oRes.fm = (undefined !== this.fm) ? this.fm.clone() : undefined;
|
|
oRes.font = (undefined !== this.font) ? this.font.clone() : undefined;
|
|
oRes.fsz = this.fsz;
|
|
oRes.va = this.va;
|
|
oRes.nl = this.nl;
|
|
oRes.hp = this.hp;
|
|
oRes.delta = this.delta;
|
|
oRes.skip = this.skip;
|
|
oRes.repeat = this.repeat;
|
|
oRes.total = this.total;
|
|
oRes.wrd = this.wrd;
|
|
return oRes;
|
|
};
|
|
|
|
/**
|
|
*
|
|
* @constructor
|
|
*/
|
|
function FragmentShaper() {
|
|
AscFonts.CTextShaper.call(this);
|
|
|
|
this.font = null;
|
|
this.stringRenderer = null;
|
|
}
|
|
FragmentShaper.prototype = Object.create(AscFonts.CTextShaper.prototype);
|
|
FragmentShaper.prototype.constructor = FragmentShaper;
|
|
FragmentShaper.prototype.GetCodePoint = function(oItem)
|
|
{
|
|
return oItem.char;
|
|
};
|
|
FragmentShaper.prototype.GetTextScript = function(nUnicode)
|
|
{
|
|
if (0x060C <= nUnicode && nUnicode <= 0x074A)
|
|
return AscFonts.HB_SCRIPT.HB_SCRIPT_ARABIC;
|
|
|
|
return AscFonts.hb_get_script_by_unicode(nUnicode);
|
|
};
|
|
FragmentShaper.prototype.shapeFragment = function(chars, font, stringRenderer, beginIndex) {
|
|
this.font = font;
|
|
this.stringRenderer = stringRenderer;
|
|
|
|
this.StartString();
|
|
for (let i = 0; i < chars.length; ++i) {
|
|
let char = chars[i];
|
|
let isNL = stringRenderer.codesHypNL[char];
|
|
let isSP = !isNL ? stringRenderer.codesHypSp[char] : false;
|
|
|
|
if (isNL || isSP) {
|
|
this.FlushWord();
|
|
this.AppendToString({idx: beginIndex + i, char: char});
|
|
} else {
|
|
this.AppendToString({idx: beginIndex + i, char: char});
|
|
}
|
|
}
|
|
|
|
this.FlushWord();
|
|
};
|
|
FragmentShaper.prototype.GetFontSlot = function() {
|
|
return AscWord.fontslot_ASCII;
|
|
};
|
|
FragmentShaper.prototype.GetFontInfo = function() {
|
|
return {
|
|
Name : this.font.getName(),
|
|
Size : this.font.getSize(),
|
|
Style : (this.font.getBold() ? 1 : 0) | (this.font.getItalic() ? 2 : 0)
|
|
};
|
|
};
|
|
FragmentShaper.prototype.FlushGrapheme = function(grapheme, width, codePointCount, isLigature) {
|
|
if (codePointCount <= 0)
|
|
return;
|
|
|
|
let charIndex = 0;
|
|
|
|
if (this.IsRtlDirection())
|
|
{
|
|
if (this.BufferIndex - codePointCount < 0)
|
|
return;
|
|
|
|
this.BufferIndex -= codePointCount;
|
|
charIndex = this.BufferIndex;
|
|
}
|
|
else
|
|
{
|
|
if (this.BufferIndex + codePointCount - 1 >= this.Buffer.length)
|
|
return;
|
|
|
|
charIndex = this.BufferIndex;
|
|
this.BufferIndex += codePointCount;
|
|
}
|
|
let _width = asc_round(width * this.font.getSize() / 25.4 * this.stringRenderer.drawingCtx.getPPIY());
|
|
|
|
let w = Math.trunc(_width / codePointCount);
|
|
let r = Math.max(0, _width - w * codePointCount);
|
|
|
|
|
|
if (1 === codePointCount)
|
|
{
|
|
this.private_HandleItem(this.Buffer[charIndex], grapheme, w);
|
|
}
|
|
else
|
|
{
|
|
if (this.IsRtlDirection())
|
|
{
|
|
this.private_HandleItem(this.Buffer[charIndex], AscFonts.NO_GRAPHEME, w);
|
|
this.private_HandleItem(this.Buffer[charIndex + codePointCount - 1], grapheme, w);
|
|
}
|
|
else
|
|
{
|
|
this.private_HandleItem(this.Buffer[charIndex], grapheme, w);
|
|
this.private_HandleItem(this.Buffer[charIndex + codePointCount - 1], AscFonts.NO_GRAPHEME, w);
|
|
}
|
|
|
|
for (let nIndex = 1; nIndex < codePointCount - 1; ++nIndex)
|
|
{
|
|
++charIndex;
|
|
this.private_HandleItem(this.Buffer[charIndex], AscFonts.NO_GRAPHEME, w + (r ? 1 : 0));
|
|
if (r)
|
|
--r;
|
|
}
|
|
}
|
|
};
|
|
FragmentShaper.prototype.private_HandleItem = function(oItem, grapheme, w) {
|
|
|
|
let st = this.stringRenderer;
|
|
let pr = st._getCharPropAt(oItem.idx);
|
|
pr.grapheme = grapheme;
|
|
pr.idx = oItem.idx;
|
|
st.charWidths[oItem.idx] = w;
|
|
st.chars[oItem.idx] = oItem.char;
|
|
|
|
};
|
|
|
|
/**
|
|
* Formatted text render
|
|
* -----------------------------------------------------------------------------
|
|
* @constructor
|
|
* @param {DrawingContext} drawingCtx Context for drawing on
|
|
*
|
|
* @memberOf Asc
|
|
*/
|
|
function StringRender(drawingCtx) {
|
|
this.drawingCtx = drawingCtx;
|
|
this.fragmentShaper = new FragmentShaper();
|
|
|
|
this.drawState = new TableCellDrawState(this);
|
|
|
|
/** @type Array */
|
|
this.fragments = undefined;
|
|
|
|
/** @type Object */
|
|
this.flags = undefined;
|
|
|
|
/** @type String */
|
|
this.chars = [];
|
|
this.charWidths = [];
|
|
this.charProps = [];
|
|
this.lines = [];
|
|
this.angle = 0;
|
|
|
|
this.codesNL = {0xD: 1, 0xA: 1};
|
|
|
|
this.codesSpace = {
|
|
0xA: 1,
|
|
0xD: 1,
|
|
0x2028: 1,
|
|
0x2029: 1,
|
|
0x9: 1,
|
|
0xB: 1,
|
|
0xC: 1,
|
|
0x0020: 1,
|
|
0x2000: 1,
|
|
0x2001: 1,
|
|
0x2002: 1,
|
|
0x2003: 1,
|
|
0x2004: 1,
|
|
0x2005: 1,
|
|
0x2006: 1,
|
|
0x2008: 1,
|
|
0x2009: 1,
|
|
0x200A: 1,
|
|
0x200B: 1,
|
|
0x205F: 1,
|
|
0x3000: 1
|
|
};
|
|
|
|
this.codesReplaceNL = {};
|
|
|
|
this.codesHypNL = {
|
|
0xA: 1, 0xD: 1, 0x2028: 1, 0x2029: 1
|
|
};
|
|
|
|
this.codesHypSp = {
|
|
0x9: 1,
|
|
0xB: 1,
|
|
0xC: 1,
|
|
0x0020: 1,
|
|
0x2000: 1,
|
|
0x2001: 1,
|
|
0x2002: 1,
|
|
0x2003: 1,
|
|
0x2004: 1,
|
|
0x2005: 1,
|
|
0x2006: 1,
|
|
0x2008: 1,
|
|
0x2009: 1,
|
|
0x200A: 1,
|
|
0x200B: 1,
|
|
0x205F: 1,
|
|
0x3000: 1
|
|
};
|
|
|
|
this.codesHyphen = {
|
|
0x002D: 1, 0x00AD: 1, 0x2010: 1, 0x2012: 1, 0x2013: 1, 0x2014: 1
|
|
};
|
|
|
|
this.clipRect = {
|
|
x: 0,
|
|
y: 0,
|
|
w: 0,
|
|
h: 0,
|
|
use: false
|
|
};
|
|
|
|
// For replacing invisible chars while rendering
|
|
/** @type RegExp */
|
|
this.reNL = /[\r\n]/;
|
|
/** @type RegExp */
|
|
//this.reSpace = /[\n\r\u2028\u2029\t\v\f\u0020\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2008\u2009\u200A\u200B\u205F\u3000]/;
|
|
/** @type RegExp */
|
|
this.reReplaceNL = /\r?\n|\r/g;
|
|
|
|
// For hyphenation
|
|
/** @type RegExp */
|
|
//this.reHypNL = /[\n\r\u2028\u2029]/;
|
|
/** @type RegExp */
|
|
//this.reHypSp = /[\t\v\f\u0020\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2008\u2009\u200A\u200B\u205F\u3000]/;
|
|
/** @type RegExp */
|
|
//this.reHyphen = /[\u002D\u00AD\u2010\u2012\u2013\u2014]/;
|
|
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Setups one or more strings to process on
|
|
* @param {String|Array} fragments A simple string or array of formatted strings AscCommonExcel.Fragment
|
|
* @param {AscCommonExcel.CellFlags} flags Optional.
|
|
* @return {StringRender} Returns 'this' to allow chaining
|
|
*/
|
|
StringRender.prototype.setString = function (fragments, flags) {
|
|
this.fragments = [];
|
|
if (asc_typeof(fragments) === "string") {
|
|
var newFragment = new AscCommonExcel.Fragment();
|
|
newFragment.setFragmentText(fragments);
|
|
newFragment.format = new AscCommonExcel.Font();
|
|
this.fragments.push(newFragment);
|
|
} else {
|
|
for (var i = 0; i < fragments.length; ++i) {
|
|
this.fragments.push(fragments[i].clone());
|
|
}
|
|
}
|
|
this.flags = flags;
|
|
this._reset();
|
|
this._setFont(this.drawingCtx, AscCommonExcel.g_oDefaultFormat.Font);
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Применяем только трансформации поворота в области
|
|
* @param {drawingCtx} drawingCtx
|
|
* @param {type} angle Угол поворота в градусах
|
|
* @param {Number} x
|
|
* @param {Number} y
|
|
* @param {Number} dx
|
|
* @param {Number} dy
|
|
* */
|
|
StringRender.prototype.rotateAtPoint = function (drawingCtx, angle, x, y, dx, dy) {
|
|
var m = new asc.Matrix();
|
|
m.rotate(angle, 0);
|
|
var mbt = new asc.Matrix();
|
|
|
|
if (null === drawingCtx) {
|
|
mbt.translate(x + dx, y + dy);
|
|
|
|
this.drawingCtx.setTextTransform(m.sx, m.shy, m.shx, m.sy, m.tx, m.ty);
|
|
this.drawingCtx.setTransform(mbt.sx, mbt.shy, mbt.shx, mbt.sy, mbt.tx, mbt.ty);
|
|
this.drawingCtx.updateTransforms();
|
|
} else {
|
|
|
|
mbt.translate((x + dx) * AscCommonExcel.vector_koef, (y + dy) * AscCommonExcel.vector_koef);
|
|
mbt.multiply(m, 0);
|
|
|
|
drawingCtx.setTransform(mbt.sx, mbt.shy, mbt.shx, mbt.sy, mbt.tx, mbt.ty);
|
|
}
|
|
this.removeClipRect();
|
|
return this;
|
|
};
|
|
|
|
StringRender.prototype.resetTransform = function (drawingCtx) {
|
|
if (null === drawingCtx) {
|
|
this.drawingCtx.resetTransforms();
|
|
} else {
|
|
var m = new asc.Matrix();
|
|
drawingCtx.setTransform(m.sx, m.shy, m.shx, m.sy, m.tx, m.ty);
|
|
}
|
|
|
|
this.angle = 0;
|
|
};
|
|
|
|
/**
|
|
* @param {Number} angle
|
|
* @param {Number} w
|
|
* @param {Number} h
|
|
* @param {Number} textW
|
|
* @param {String} alignHorizontal
|
|
* @param {String} alignVertical
|
|
* @param {Number} maxWidth
|
|
*/
|
|
StringRender.prototype.getTransformBound = function (angle, w, h, textW, alignHorizontal, alignVertical, maxWidth) {
|
|
var ctx = this.drawingCtx;
|
|
|
|
// TODO: добавить padding по сторонам
|
|
|
|
this.angle = 0; // angle;
|
|
|
|
var dx = 0, dy = 0, offsetX = 0, // смещение BB
|
|
|
|
tm = this._doMeasure(maxWidth),
|
|
|
|
mul = (90 - (Math.abs(angle))) / 90,
|
|
|
|
angleSin = Math.sin(angle * Math.PI / 180.0),
|
|
angleCos = Math.cos(angle * Math.PI / 180.0),
|
|
|
|
posh = (angle === 90 || angle === -90) ? textW : Math.abs(angleSin * textW),
|
|
posv = (angle === 90 || angle === -90) ? 0 : Math.abs(angleCos * textW),
|
|
|
|
isHorzLeft = (AscCommon.align_Left === alignHorizontal),
|
|
isHorzCenter = (AscCommon.align_Center === alignHorizontal),
|
|
isHorzRight = (AscCommon.align_Right === alignHorizontal),
|
|
|
|
isVertBottom = (Asc.c_oAscVAlign.Bottom === alignVertical),
|
|
isVertCenter = (Asc.c_oAscVAlign.Center === alignVertical || Asc.c_oAscVAlign.Dist === alignVertical || Asc.c_oAscVAlign.Just === alignVertical),
|
|
isVertTop = (Asc.c_oAscVAlign.Top === alignVertical);
|
|
|
|
|
|
var _height = tm.height * ctx.getZoom();
|
|
if (isVertBottom) {
|
|
if (angle < 0) {
|
|
if (isHorzLeft) {
|
|
dx = -(angleSin * _height);
|
|
} else if (isHorzCenter) {
|
|
dx = (w - angleSin * _height - posv) / 2;
|
|
offsetX = -(w - posv) / 2 - angleSin * _height / 2;
|
|
} else if (isHorzRight) {
|
|
dx = w - posv + 2;
|
|
offsetX = -(w - posv) - angleSin * _height - 2;
|
|
}
|
|
} else {
|
|
if (isHorzLeft) {
|
|
|
|
} else if (isHorzCenter) {
|
|
dx = (w - angleSin * _height - posv) / 2;
|
|
offsetX = -(w - posv) / 2 + angleSin * _height / 2;
|
|
} else if (isHorzRight) {
|
|
dx = w - posv + 1 + 1 - _height * angleSin;
|
|
offsetX = -w - posv + 1 + 1 - _height * angleSin;
|
|
}
|
|
}
|
|
|
|
if (posh < h) {
|
|
if (angle < 0) {
|
|
dy = h - (posh + angleCos * _height);
|
|
} else {
|
|
dy = h - angleCos * _height;
|
|
}
|
|
} else {
|
|
if (angle > 0) {
|
|
dy = h - angleCos * _height;
|
|
}
|
|
}
|
|
} else if (isVertCenter) {
|
|
|
|
if (angle < 0) {
|
|
if (isHorzLeft) {
|
|
dx = -(angleSin * _height);
|
|
} else if (isHorzCenter) {
|
|
dx = (w - angleSin * _height - posv) / 2;
|
|
offsetX = -(w - posv) / 2 - angleSin * _height / 2;
|
|
} else if (isHorzRight) {
|
|
dx = w - posv + 2;
|
|
offsetX = -(w - posv) - angleSin * _height - 2;
|
|
}
|
|
} else {
|
|
if (isHorzLeft) {
|
|
|
|
} else if (isHorzCenter) {
|
|
dx = (w - angleSin * _height - posv) / 2;
|
|
offsetX = -(w - posv) / 2 + angleSin * _height / 2;
|
|
} else if (isHorzRight) {
|
|
dx = w - posv + 1 + 1 - _height * angleSin;
|
|
offsetX = -w - posv + 1 + 1 - _height * angleSin;
|
|
}
|
|
}
|
|
|
|
//
|
|
|
|
if (posh < h) {
|
|
if (angle < 0) {
|
|
dy = (h - posh - angleCos * _height) * 0.5;
|
|
} else {
|
|
dy = (h + posh - angleCos * _height) * 0.5;
|
|
}
|
|
} else {
|
|
if (angle > 0) {
|
|
dy = h - angleCos * _height;
|
|
}
|
|
}
|
|
} else if (isVertTop) {
|
|
|
|
if (angle < 0) {
|
|
if (isHorzLeft) {
|
|
dx = -(angleSin * _height);
|
|
} else if (isHorzCenter) {
|
|
dx = (w - angleSin * _height - posv) / 2;
|
|
offsetX = -(w - posv) / 2 - angleSin * _height / 2;
|
|
} else if (isHorzRight) {
|
|
dx = w - posv + 2;
|
|
offsetX = -(w - posv) - angleSin * _height - 2;
|
|
}
|
|
} else {
|
|
if (isHorzLeft) {
|
|
} else if (isHorzCenter) {
|
|
dx = (w - angleSin * _height - posv) / 2;
|
|
offsetX = -(w - posv) / 2 + angleSin * _height / 2;
|
|
} else if (isHorzRight) {
|
|
dx = w - posv + 1 + 1 - _height * angleSin;
|
|
offsetX = -w - posv + 1 + 1 - _height * angleSin;
|
|
}
|
|
|
|
dy = Math.min(h + _height * angleCos, posh);
|
|
}
|
|
}
|
|
|
|
var bound = {dx: dx, dy: dy, height: 0, width: 0, offsetX: offsetX};
|
|
|
|
if (angle === 90 || angle === -90) {
|
|
bound.width = _height;
|
|
bound.height = textW;
|
|
} else {
|
|
bound.height = Math.abs(angleSin * textW) + Math.abs(angleCos * _height);
|
|
bound.width = Math.abs(angleCos * textW) + Math.abs(angleSin * _height);
|
|
}
|
|
|
|
return bound;
|
|
};
|
|
|
|
/**
|
|
* Measures string that was setup by 'setString' method
|
|
* @param {Number} maxWidth Optional. Text width restriction
|
|
* @return {Asc.TextMetrics} Returns text metrics or null. @see Asc.TextMetrics
|
|
*/
|
|
StringRender.prototype.measure = function (maxWidth) {
|
|
return this._doMeasure(maxWidth);
|
|
};
|
|
|
|
/**
|
|
* Draw string that was setup by methods 'setString' or 'measureString'
|
|
* @param {drawingCtx} drawingCtx
|
|
* @param {Number} x Left of the text rect
|
|
* @param {Number} y Top of the text rect
|
|
* @param {Number} maxWidth Text width restriction
|
|
* @param {String} textColor Default text color for formatless string
|
|
* @return {StringRender} Returns 'this' to allow chaining
|
|
*/
|
|
StringRender.prototype.render = function (drawingCtx, x, y, maxWidth, textColor) {
|
|
this._doRender(drawingCtx, x, y, maxWidth, textColor);
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Measures string
|
|
* @param {String|Array} fragments A simple string or array of formatted strings AscCommonExcel.Fragment
|
|
* @param {AscCommonExcel.CellFlags} [flags] Optional.
|
|
* @param {Number} [maxWidth] Optional. Text width restriction
|
|
* @return {Asc.TextMetrics} Returns text metrics or null. @see Asc.TextMetrics
|
|
*/
|
|
StringRender.prototype.measureString = function (fragments, flags, maxWidth) {
|
|
if (fragments) {
|
|
this.setString(fragments, flags);
|
|
}
|
|
return this._doMeasure(maxWidth);
|
|
};
|
|
|
|
/**
|
|
* Returns the width of the widest char in the string has been measured
|
|
*/
|
|
StringRender.prototype.getWidestCharWidth = function () {
|
|
return this.charWidths.reduce(function (p, c) {
|
|
return p < c ? c : p;
|
|
}, 0);
|
|
};
|
|
|
|
StringRender.prototype._reset = function () {
|
|
this.chars = [];
|
|
this.charWidths = [];
|
|
this.charProps = [];
|
|
this.lines = [];
|
|
};
|
|
|
|
/**
|
|
* @param {String} fragment
|
|
* @param {Boolean} wrap
|
|
* @return {String} Returns filtered fragment
|
|
*/
|
|
StringRender.prototype._filterText = function (fragment, wrap) {
|
|
var s = fragment;
|
|
if (s.search(this.reNL) >= 0) {
|
|
s = s.replace(this.reReplaceNL, wrap ? "\n" : "");
|
|
}
|
|
return s;
|
|
};
|
|
|
|
StringRender.prototype._filterChars = function (chars, wrap) {
|
|
var res = [];
|
|
if (chars) {
|
|
for (var i = 0; i < chars.length; i++) {
|
|
if (0xD === chars[i] && 0xA === chars[i + 1]) {
|
|
//\r\n
|
|
if (wrap) {
|
|
res.push(0xA);
|
|
}
|
|
i++;
|
|
} else if (0xA === chars[i]) {
|
|
//\r
|
|
if (wrap) {
|
|
res.push(0xA);
|
|
}
|
|
} else {
|
|
res.push(chars[i]);
|
|
}
|
|
}
|
|
}
|
|
return res;
|
|
};
|
|
|
|
/**
|
|
* @param {Number} startCh
|
|
* @param {Number} endCh
|
|
* @return {Number}
|
|
*/
|
|
StringRender.prototype._calcCharsWidth = function (startCh, endCh) {
|
|
for (var w = 0, i = startCh; i <= endCh; ++i) {
|
|
w += this.charWidths[i];
|
|
}
|
|
return w;
|
|
};
|
|
|
|
/**
|
|
* @param {Number} startPos
|
|
* @param {Number} endPos
|
|
* @return {Number}
|
|
*/
|
|
StringRender.prototype._calcLineWidth = function (startPos, endPos) {
|
|
var wrap = this.flags && (this.flags.wrapText || this.flags.wrapOnlyNL || this.flags.wrapOnlyCE);
|
|
var isAtEnd, j, chProp, tw;
|
|
|
|
if (endPos === undefined || endPos < 0) {
|
|
// search for end of line
|
|
for (j = startPos + 1; j < this.chars.length; ++j) {
|
|
chProp = this.charProps[j];
|
|
if (chProp && (chProp.nl || chProp.hp)) {
|
|
break;
|
|
}
|
|
}
|
|
endPos = j - 1;
|
|
}
|
|
|
|
for (j = endPos, tw = 0, isAtEnd = true; j >= startPos; --j) {
|
|
if (isAtEnd) {
|
|
// skip space char at end of line
|
|
if ((wrap) && this.codesSpace[this.chars[j]]) {
|
|
continue;
|
|
}
|
|
isAtEnd = false;
|
|
}
|
|
tw += this.charWidths[j];
|
|
}
|
|
|
|
return tw;
|
|
};
|
|
|
|
StringRender.prototype._calcLineMetrics = function (f, va, fm) {
|
|
var l = new lineMetrics();
|
|
|
|
if (!va) {
|
|
var _a = Math.max(0, asc.ceil(fm.nat_y1 * f / fm.nat_scale));
|
|
var _d = Math.max(0, asc.ceil(-fm.nat_y2 * f / fm.nat_scale)) + 1; // 1 px for border
|
|
|
|
l.th = _a + _d;
|
|
l.bl = _a;
|
|
l.a = _a;
|
|
l.d = _d;
|
|
} else {
|
|
var ppi = 96;
|
|
var hpt = f * 1.275;
|
|
var fpx = f * ppi / 72;
|
|
var topt = 72 / ppi;
|
|
|
|
var h;
|
|
var a = asc_round(fpx) * topt;
|
|
var d;
|
|
|
|
var a_2 = asc_round(fpx / 2) * topt;
|
|
|
|
var h_2_3;
|
|
var a_2_3 = asc_round(fpx * 2 / 3) * topt;
|
|
var d_2_3;
|
|
|
|
var x = a_2 + a_2_3;
|
|
|
|
if (va === AscCommon.vertalign_SuperScript) {
|
|
h = hpt;
|
|
d = h - a;
|
|
|
|
l.th = x + d;
|
|
l.bl = x;
|
|
l.bl2 = a_2_3;
|
|
l.a = fm.ascender + a_2; // >0
|
|
l.d = fm.descender - a_2; // <0
|
|
} else if (va === AscCommon.vertalign_SubScript) {
|
|
h_2_3 = hpt * 2 / 3;
|
|
d_2_3 = h_2_3 - a_2_3;
|
|
l.th = x + d_2_3;
|
|
l.bl = a;
|
|
l.bl2 = x;
|
|
l.a = fm.ascender + a - x; // >0
|
|
l.d = fm.descender + x - a; // >0
|
|
}
|
|
}
|
|
|
|
return l;
|
|
};
|
|
StringRender.prototype._calcLineMetrics2 = function (f, va, fm) {
|
|
var l = new lineMetrics();
|
|
|
|
var a = Math.max(0, asc.ceil(fm.nat_y1 * f / fm.nat_scale));
|
|
var d = Math.max(0, asc.ceil(-fm.nat_y2 * f / fm.nat_scale)) + 1; // 1 px for border
|
|
|
|
/*
|
|
// ToDo
|
|
if (va) {
|
|
var k = (AscCommon.vertalign_SuperScript === va) ? AscCommon.vaKSuper : AscCommon.vaKSub;
|
|
d += asc.ceil((a + d) * k);
|
|
f = asc.ceil(f * 2 / 3 / 0.5) * 0.5; // Round 0.5
|
|
a = Math.max(0, asc.ceil(fm.nat_y1 * f / fm.nat_scale));
|
|
}
|
|
*/
|
|
|
|
l.th = a + d;
|
|
l.bl = a;
|
|
l.a = a;
|
|
l.d = d;
|
|
|
|
return l;
|
|
};
|
|
|
|
StringRender.prototype.calcDelta = function (vnew, vold) {
|
|
return vnew > vold ? vnew - vold : 0;
|
|
};
|
|
|
|
/**
|
|
* @param {Boolean} [dontCalcRepeatChars]
|
|
* @return {Asc.TextMetrics}
|
|
*/
|
|
StringRender.prototype._calcTextMetrics = function (dontCalcRepeatChars) {
|
|
var self = this, i = 0, p, p_, lm, beg = 0;
|
|
var l = new LineInfo(), TW = 0, TH = 0, BL = 0;
|
|
|
|
function addLine(b, e) {
|
|
if (-1 !== b)
|
|
l.tw += self._calcLineWidth(b, e);
|
|
l.beg = b;
|
|
l.end = e < b ? b : e;
|
|
self.lines.push(l);
|
|
if (TW < l.tw) {
|
|
TW = l.tw;
|
|
}
|
|
BL = TH + l.bl;
|
|
TH += l.th;
|
|
}
|
|
|
|
if (0 >= this.chars.length) {
|
|
p = this.charProps[0];
|
|
if (p && p.font) {
|
|
lm = this._calcLineMetrics(p.fsz !== undefined ? p.fsz : p.font.getSize(), p.va, p.fm);
|
|
l.assign(lm);
|
|
addLine(-1, -1);
|
|
l.beg = l.end = 0;
|
|
}
|
|
} else {
|
|
for (; i < this.chars.length; ++i) {
|
|
p = this.charProps[i];
|
|
|
|
// if font has been changed than calc and update line height and etc.
|
|
if (p && p.font) {
|
|
lm = this._calcLineMetrics(p.fsz !== undefined ? p.fsz : p.font.getSize(), p.va, p.fm);
|
|
if (i === 0) {
|
|
l.assign(lm);
|
|
} else {
|
|
l.th += this.calcDelta(lm.bl, l.bl) + this.calcDelta(lm.th - lm.bl, l.th - l.bl);
|
|
l.bl += this.calcDelta(lm.bl, l.bl);
|
|
l.a += this.calcDelta(lm.a, l.a);
|
|
l.d += this.calcDelta(lm.d, l.d);
|
|
}
|
|
p.lm = lm;
|
|
p_ = p;
|
|
}
|
|
|
|
// process 'repeat char' marker
|
|
if (dontCalcRepeatChars && p && p.repeat) {
|
|
l.tw -= this._calcCharsWidth(i, i + p.total);
|
|
}
|
|
|
|
// process 'new line' marker
|
|
if (p && (p.nl || p.hp)) {
|
|
addLine(beg, i);
|
|
beg = i + (p.nl ? 1 : 0);
|
|
lm = this._calcLineMetrics(p_.fsz !== undefined ? p_.fsz : p_.font.getSize(), p_.va, p_.fm);
|
|
l = new LineInfo(lm);
|
|
}
|
|
}
|
|
if (beg <= i) {
|
|
// add last line of text
|
|
addLine(beg, i - 1);
|
|
}
|
|
}
|
|
return new asc.TextMetrics(TW, TH, 0, BL, 0, 0);
|
|
};
|
|
|
|
StringRender.prototype._getRepeatCharPos = function () {
|
|
var charProp;
|
|
for (var i = 0; i < this.chars.length; ++i) {
|
|
charProp = this.charProps[i];
|
|
if (charProp && charProp.repeat)
|
|
return i;
|
|
}
|
|
return -1;
|
|
};
|
|
|
|
/**
|
|
* @param {Number} maxWidth
|
|
*/
|
|
StringRender.prototype._insertRepeatChars = function (maxWidth) {
|
|
var self = this, width, w, pos, charProp;
|
|
|
|
function insertRepeatChars() {
|
|
if (0 === charProp.total)
|
|
return; // Символ уже изначально лежит в строке и в списке
|
|
var repeatEnd = pos + charProp.total;
|
|
self.chars = [].concat(
|
|
self.chars.slice(0, repeatEnd),
|
|
self.chars.slice(pos, pos + 1),
|
|
self.chars.slice(repeatEnd));
|
|
|
|
self.charWidths = [].concat(
|
|
self.charWidths.slice(0, repeatEnd),
|
|
self.charWidths.slice(pos, pos + 1),
|
|
self.charWidths.slice(repeatEnd));
|
|
|
|
self.charProps = [].concat(
|
|
self.charProps.slice(0, repeatEnd),
|
|
self.charProps.slice(pos, pos + 1),
|
|
self.charProps.slice(repeatEnd));
|
|
}
|
|
|
|
function removeRepeatChar() {
|
|
self.chars = [].concat(
|
|
self.chars.slice(0, pos),
|
|
self.chars.slice(pos + 1));
|
|
|
|
self.charWidths = [].concat(
|
|
self.charWidths.slice(0, pos),
|
|
self.charWidths.slice(pos + 1));
|
|
|
|
self.charProps = [].concat(
|
|
self.charProps.slice(0, pos),
|
|
self.charProps.slice(pos + 1));
|
|
}
|
|
|
|
width = this._calcTextMetrics(true).width;
|
|
pos = this._getRepeatCharPos();
|
|
if (-1 === pos)
|
|
return;
|
|
w = this._calcCharsWidth(pos, pos);
|
|
charProp = this.charProps[pos];
|
|
|
|
while (charProp.total * w + width + w <= maxWidth) {
|
|
insertRepeatChars();
|
|
charProp.total += 1;
|
|
if (w === 0) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (0 === charProp.total)
|
|
removeRepeatChar();
|
|
|
|
this.lines = [];
|
|
};
|
|
|
|
StringRender.prototype._getCharPropAt = function (index) {
|
|
var prop = this.charProps[index];
|
|
if (!prop) {
|
|
prop = this.charProps[index] = new charProperties();
|
|
}
|
|
return prop;
|
|
};
|
|
|
|
StringRender.prototype._getGraphemeDelta = function(grapheme, fontSize) {
|
|
let ppiy = this.drawingCtx.getPPIY();
|
|
let width = AscFonts.GetGraphemeWidth(grapheme) * ppiy / 25.4 * fontSize;
|
|
let bbox = AscFonts.GetGraphemeBBox(grapheme, fontSize, ppiy);
|
|
return bbox.maxX - bbox.minX + 1 - width;
|
|
};
|
|
|
|
/**
|
|
* @param {Number} maxWidth
|
|
* @return {Asc.TextMetrics}
|
|
*/
|
|
StringRender.prototype._measureChars = function (maxWidth) {
|
|
var self = this;
|
|
var ctx = this.drawingCtx;
|
|
var font = ctx.font;
|
|
var wrap = this.flags && (this.flags.wrapText || this.flags.wrapOnlyCE) && !this.flags.isNumberFormat;
|
|
var wrapNL = this.flags && this.flags.wrapOnlyNL;
|
|
var verticalText = this.flags && this.flags.verticalText;
|
|
var hasRepeats = false;
|
|
var i, j, fr, fmt, chars, p, p_ = {}, pIndex, startCh;
|
|
var tw = 0, nlPos = 0, isEastAsian, hpPos = undefined, isSP_ = true, delta = 0;
|
|
let frShaper = this.fragmentShaper;
|
|
|
|
this.drawState.reset(null, null, this.flags, this.angle);
|
|
|
|
function measureFragment(_chars, format) {
|
|
|
|
let chPos = self.chars.length;
|
|
let fontSize = format.getSize();
|
|
frShaper.shapeFragment(_chars, format, self, chPos);
|
|
|
|
var chc, chw, isNL, isSP, isHP;
|
|
for (; chPos < self.chars.length; ++chPos) {
|
|
chc = self.chars[chPos];
|
|
chw = self.charWidths[chPos];
|
|
|
|
isNL = self.codesHypNL[chc];
|
|
isSP = !isNL ? self.codesHypSp[chc] : false;
|
|
|
|
// if 'wrap flag' is set
|
|
if (wrap || wrapNL || verticalText) {
|
|
isHP = !isSP && !isNL ? self.codesHyphen[chc] : false;
|
|
isEastAsian = AscCommon.isEastAsianScript(chc);
|
|
if (verticalText) {
|
|
// ToDo verticalText and new line or space
|
|
} else if (isNL) {
|
|
// add new line marker
|
|
nlPos = chPos;
|
|
self._getCharPropAt(nlPos).nl = true;
|
|
self._getCharPropAt(nlPos).delta = delta;
|
|
chc = 0xA0;
|
|
chw = 0;
|
|
tw = 0;
|
|
hpPos = undefined;
|
|
self.charWidths[chPos] = 0;
|
|
} else if (isSP || isHP) {
|
|
// move hyphenation position
|
|
hpPos = chPos + 1;
|
|
} else if (isEastAsian) {
|
|
if (0 !== chPos && !(AscCommon.g_aPunctuation[self.chars[chPos - 1]] &
|
|
AscCommon.PUNCTUATION_FLAG_CANT_BE_AT_END_E) &&
|
|
!(AscCommon.g_aPunctuation[chc] & AscCommon.PUNCTUATION_FLAG_CANT_BE_AT_BEGIN_E)) {
|
|
// move hyphenation position
|
|
hpPos = chPos;
|
|
}
|
|
}
|
|
|
|
if (chPos !== nlPos && ((wrap && !isSP && tw + chw > maxWidth) || (verticalText && !self._isCombinedChar(chPos)))) {
|
|
// add hyphenation marker
|
|
nlPos = hpPos !== undefined ? hpPos : chPos;
|
|
self._getCharPropAt(nlPos).hp = true;
|
|
self._getCharPropAt(nlPos).delta = delta;
|
|
tw = self._calcCharsWidth(nlPos, chPos - 1);
|
|
hpPos = undefined;
|
|
}
|
|
|
|
if (isEastAsian) {
|
|
// move hyphenation position
|
|
if (chPos < self.chars.length - 1 && !(AscCommon.g_aPunctuation[self.chars[chPos + 1]] &
|
|
AscCommon.PUNCTUATION_FLAG_CANT_BE_AT_BEGIN_E) &&
|
|
!(AscCommon.g_aPunctuation[chc] & AscCommon.PUNCTUATION_FLAG_CANT_BE_AT_END_E)) {
|
|
hpPos = chPos + 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (isSP_ && !isSP && !isNL) {
|
|
// add word beginning marker
|
|
self._getCharPropAt(chPos).wrd = true;
|
|
}
|
|
|
|
tw += chw;
|
|
|
|
isSP_ = isSP || isNL;
|
|
|
|
if (isSP || isNL) {
|
|
delta = 0;
|
|
} else if (AscFonts.NO_GRAPHEME !== self._getCharPropAt(chPos).grapheme) {
|
|
delta = self._getGraphemeDelta(self._getCharPropAt(chPos).grapheme, fontSize);
|
|
}
|
|
}
|
|
}
|
|
|
|
this._reset();
|
|
|
|
// for each text fragment
|
|
for (i = 0; i < this.fragments.length; ++i) {
|
|
startCh = this.charWidths.length;
|
|
fr = this.fragments[i];
|
|
fmt = fr.format.clone();
|
|
var va = fmt.getVerticalAlign();
|
|
|
|
//TODO пока не убрал эту регулярку, сначала перевожу в текст, потом обратно в сиволы
|
|
//TODO избавиться от регулярки!
|
|
if (fr.isInitCharCodes()) {
|
|
fr.initText();
|
|
}
|
|
chars = this._filterChars(fr.getCharCodes(), wrap || wrapNL);
|
|
//fr.initCharCodes();
|
|
|
|
pIndex = this.chars.length;
|
|
p = this.charProps[pIndex];
|
|
p = p ? p.clone() : new charProperties();
|
|
|
|
// reduce font size for subscript and superscript chars
|
|
if (va === AscCommon.vertalign_SuperScript || va === AscCommon.vertalign_SubScript) {
|
|
p.va = va;
|
|
p.fsz = fmt.getSize();
|
|
fmt.fs = p.fsz * 2 / 3;
|
|
p.font = fmt;
|
|
}
|
|
|
|
// change font on canvas
|
|
if (!fmt.isEqual(ctx.font)
|
|
|| fmt.getUnderline() !== font.getUnderline()
|
|
|| fmt.getStrikeout() !== font.getStrikeout()
|
|
|| fmt.getColor() !== p_.c) {
|
|
p.font = fmt;
|
|
}
|
|
this._setFont(ctx, fmt);
|
|
|
|
// add marker in chars flow
|
|
if (i === 0) {
|
|
p.font = fmt;
|
|
}
|
|
if (p.font) {
|
|
p.fm = ctx.getFontMetrics();
|
|
p.c = fmt.getColor();
|
|
this.charProps[pIndex] = p;
|
|
p_ = p;
|
|
}
|
|
|
|
if (fmt.getSkip()) {
|
|
this._getCharPropAt(pIndex).skip = chars.length;
|
|
}
|
|
|
|
if (fmt.getRepeat()) {
|
|
if (hasRepeats)
|
|
throw new Error("Repeat should occur no more than once");
|
|
|
|
this._getCharPropAt(pIndex).repeat = true;
|
|
this._getCharPropAt(pIndex).total = 0;
|
|
hasRepeats = true;
|
|
}
|
|
|
|
if (chars.length < 1) {
|
|
continue;
|
|
}
|
|
measureFragment(chars, fmt);
|
|
|
|
// для italic текста прибавляем к концу строки разницу между charWidth и BBox
|
|
for (j = startCh; font.getItalic() && j < this.charWidths.length; ++j) {
|
|
if (this.charProps[j] && this.charProps[j].delta && j > 0) {
|
|
if (this.charWidths[j - 1] > 0) {
|
|
this.charWidths[j - 1] += this.charProps[j].delta;
|
|
} else if (j > 1) {
|
|
this.charWidths[j - 2] += this.charProps[j].delta;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (0 !== this.chars.length && this.charProps[this.chars.length] !== undefined) {
|
|
delete this.charProps[this.chars.length];
|
|
} else if (font.getItalic()) {
|
|
// для italic текста прибавляем к концу текста разницу между charWidth и BBox
|
|
this.charWidths[this.charWidths.length - 1] += delta;
|
|
}
|
|
|
|
if (hasRepeats) {
|
|
if (maxWidth === undefined) {
|
|
throw new Error("Undefined width of cell width Numeric Format");
|
|
}
|
|
this._insertRepeatChars(maxWidth);
|
|
}
|
|
|
|
return this._calcTextMetrics();
|
|
};
|
|
|
|
/**
|
|
* @param {Number} maxWidth
|
|
* @return {Asc.TextMetrics}
|
|
*/
|
|
StringRender.prototype._doMeasure = function (maxWidth) {
|
|
var ratio, format, size, canReduce = true, minSize = 2.5;
|
|
var tm = this._measureChars(maxWidth);
|
|
while (this.flags && this.flags.shrinkToFit && tm.width > maxWidth && canReduce) {
|
|
canReduce = false;
|
|
ratio = maxWidth / tm.width;
|
|
for (var i = 0; i < this.fragments.length; ++i) {
|
|
format = this.fragments[i].format;
|
|
size = Math.max(minSize, Math.floor(format.getSize() * ratio * 2) / 2);
|
|
format.setSize(size);
|
|
if (minSize < size) {
|
|
canReduce = true;
|
|
}
|
|
}
|
|
tm = this._measureChars(maxWidth);
|
|
}
|
|
return tm;
|
|
};
|
|
|
|
/**
|
|
* @param {DrawingContext} drawingCtx
|
|
* @param {Number} x
|
|
* @param {Number} y
|
|
* @param {Number} maxWidth
|
|
* @param {String} textColor
|
|
*/
|
|
|
|
StringRender.prototype._doRender = function (drawingCtx, x, y, maxWidth, textColor) {
|
|
let self = this;
|
|
let ctx = drawingCtx || this.drawingCtx;
|
|
let zoom = ctx.getZoom();
|
|
let ppiy = ctx.getPPIY();
|
|
this.drawState.reset(drawingCtx, textColor, this.flags, this.angle);
|
|
let drawState = this.drawState;
|
|
let align = this.getEffectiveAlign();
|
|
let i, j, p, p_, strBeg;
|
|
let n = 0, l = this.lines[0], x1 = l ? this.initStartX(0, l, x, maxWidth) : 0, y1 = y, dx = l ? computeWordDeltaX() : 0;
|
|
|
|
ctx.setTextRotated(!!this.angle);
|
|
self.textColor = textColor;
|
|
|
|
|
|
function computeWordDeltaX() {
|
|
if (align !== AscCommon.align_Justify || n === self.lines.length - 1) {
|
|
return 0;
|
|
}
|
|
|
|
if (align === AscCommon.align_Justify) {
|
|
let wordCount = 0;
|
|
let isLastWordSpace = false;
|
|
let lastSpacesWidth = 0;
|
|
let lastSymbolWidth = 0;
|
|
|
|
for (let i = l.beg; i <= l.end; ++i) {
|
|
let p = self.charProps[i];
|
|
let isSpace = self.codesHypSp[self.chars[i]];
|
|
|
|
if (p && p.wrd && isLastWordSpace) {
|
|
++wordCount;
|
|
if (i !== l.end) {
|
|
lastSpacesWidth = 0;
|
|
} else if (!isSpace) {
|
|
lastSymbolWidth = self.charWidths[i];
|
|
}
|
|
} else if (i === l.end) {
|
|
++wordCount;
|
|
}
|
|
|
|
if (isSpace) {
|
|
lastSpacesWidth += self.charWidths[i];
|
|
}
|
|
|
|
isLastWordSpace = isSpace;
|
|
}
|
|
|
|
if (wordCount <= 1) {
|
|
return 0;
|
|
}
|
|
|
|
let rightDiff = 1;
|
|
let availableWidth = maxWidth - rightDiff - (l.tw - lastSymbolWidth - lastSpacesWidth);
|
|
return (availableWidth) / (wordCount - 1);
|
|
} else {
|
|
for (var i = l.beg, c = 0; i <= l.end; ++i) {
|
|
var p = self.charProps[i];
|
|
if (p && p.wrd) {
|
|
++c;
|
|
}
|
|
}
|
|
return c > 1 ? (maxWidth - l.tw) / (c - 1) : 0;
|
|
}
|
|
}
|
|
|
|
function renderFragment(begin, end, prop, angle) {
|
|
var dh = prop && prop.lm && prop.lm.bl2 > 0 ? prop.lm.bl2 - prop.lm.bl : 0;
|
|
var dw = self._calcCharsWidth(strBeg, end - 1);
|
|
var so = prop.font.getStrikeout();
|
|
var ul = Asc.EUnderline.underlineNone !== prop.font.getUnderline();
|
|
var isSO = so === true;
|
|
var fsz, x2, y, lw, dy, i, b, cp;
|
|
var bl = asc_round(l.bl * zoom);
|
|
|
|
if (begin > end)
|
|
return 0;
|
|
|
|
let fontSize = prop.font.getSize();
|
|
y = y1 + bl + dh;
|
|
|
|
let startX = drawState.x;
|
|
x1 = startX;
|
|
if (align !== AscCommon.align_Justify || dx < 0.000001) {
|
|
renderGraphemes(begin, end, drawState.x, y, fontSize);
|
|
} else {
|
|
for (i = b = begin; i < end; ++i) {
|
|
cp = self.charProps[i];
|
|
if (cp && cp.wrd && i > b) {
|
|
x1 = drawState.x;
|
|
renderGraphemes(b, i, drawState.x, y, fontSize);
|
|
x1 += self._calcCharsWidth(b, i - 1) + dx;
|
|
drawState.x = x1;
|
|
dw += dx;
|
|
b = i;
|
|
}
|
|
}
|
|
if (i > b) {
|
|
renderGraphemes(b, i, drawState.x, y, fontSize);
|
|
}
|
|
}
|
|
|
|
|
|
if (isSO || ul) {
|
|
if (angle && window["IS_NATIVE_EDITOR"])
|
|
ctx.nativeTextDecorationTransform(true);
|
|
|
|
x2 = startX + dw;
|
|
fsz = prop.font.getSize();
|
|
lw = asc_round(fsz * ppiy / 72 / 18) || 1;
|
|
ctx.setStrokeStyle(prop.c || textColor)
|
|
.setLineWidth(lw)
|
|
.beginPath();
|
|
dy = (lw / 2);
|
|
dy = dy >> 0;
|
|
if (ul) {
|
|
y = asc_round(y1 + bl + prop.lm.d * 0.4 * zoom);
|
|
ctx.lineHor(startX, y + dy, x2 + 1);
|
|
}
|
|
if (isSO) {
|
|
dy += 1;
|
|
y = asc_round(y1 + bl - prop.lm.a * 0.275 * zoom);
|
|
ctx.lineHor(startX, y - dy, x2 + 1);
|
|
}
|
|
ctx.stroke();
|
|
|
|
if (angle && window["IS_NATIVE_EDITOR"])
|
|
ctx.nativeTextDecorationTransform(false);
|
|
}
|
|
|
|
return dw;
|
|
}
|
|
|
|
function renderGraphemes(begin, end, x, y) {
|
|
drawState.y = y;
|
|
drawState.beginFragment(begin, end, p_);
|
|
}
|
|
|
|
|
|
drawState.beginLine(l, x1, y);
|
|
for (i = 0, strBeg = 0; i < this.chars.length; ++i) {
|
|
p = this.charProps[i];
|
|
|
|
if (p && (p.font || p.nl || p.hp || p.skip > 0)) {
|
|
if (strBeg < i) {
|
|
renderFragment(strBeg, i, p_, this.angle);
|
|
strBeg = i;
|
|
}
|
|
if (p.nl) {
|
|
strBeg += 1;
|
|
}
|
|
|
|
if (p.font) {
|
|
p_ = p;
|
|
}
|
|
if (p.skip > 0) {
|
|
j = i + p.skip - 1;
|
|
drawState.x += this._calcCharsWidth(i, j);
|
|
strBeg = j + 1;
|
|
i = j;
|
|
continue;
|
|
}
|
|
if (p.nl || p.hp) {
|
|
drawState.endLine();
|
|
y1 += asc_round(l.th * zoom);
|
|
l = self.lines[++n];
|
|
drawState.x = self.initStartX(i, l, x, maxWidth);
|
|
dx = computeWordDeltaX();
|
|
drawState.beginLine(l, drawState.x, y);
|
|
}
|
|
}
|
|
}
|
|
if (strBeg < i) {
|
|
renderFragment(strBeg, i, p_, this.angle);
|
|
}
|
|
|
|
drawState.endLine();
|
|
};
|
|
StringRender.prototype.initStartX = function (startPos, l, x, maxWidth, initAllLines) {
|
|
let align = this.getEffectiveAlign();
|
|
|
|
if (initAllLines) {
|
|
if (this.lines) {
|
|
for (let i = 0; i < this.lines.length; ++i) {
|
|
let lineWidth = this._calcLineWidth(this.lines[i].beg);
|
|
this.lines[i].initStartX(lineWidth, x, maxWidth, align);
|
|
}
|
|
}
|
|
} else {
|
|
return l.initStartX(this._calcLineWidth(startPos), x, maxWidth, align);
|
|
}
|
|
};
|
|
StringRender.prototype.getInternalState = function () {
|
|
return {
|
|
/** @type Object */
|
|
flags: this.flags,
|
|
|
|
chars: this.chars,
|
|
charWidths: this.charWidths,
|
|
charProps: this.charProps,
|
|
lines: this.lines
|
|
};
|
|
};
|
|
StringRender.prototype.restoreInternalState = function (state) {
|
|
this.flags = state.flags;
|
|
this.chars = state.chars;
|
|
this.charWidths = state.charWidths;
|
|
this.charProps = state.charProps;
|
|
this.lines = state.lines;
|
|
return this;
|
|
};
|
|
StringRender.prototype._setFont = function (ctx, font) {
|
|
let oldColor = font.c;
|
|
if(this.textColor) font.c = this.textColor;
|
|
ctx.setFont(font, this.angle);
|
|
font.c = oldColor;
|
|
};
|
|
StringRender.prototype._isCombinedChar = function(pos) {
|
|
let p = this._getCharPropAt(pos);
|
|
let c = this.chars[pos];
|
|
return !p.nl && !this.codesSpace[c] && (AscFonts.NO_GRAPHEME === p.grapheme);
|
|
};
|
|
StringRender.prototype.getEffectiveAlign = function() {
|
|
let align = this.flags ? this.flags.textAlign : null;
|
|
|
|
if (align !== null) return align;
|
|
let isRtl = this.drawState.getMainDirection() === AscBidi.DIRECTION_FLAG.RTL;
|
|
return isRtl ? AscCommon.align_Right : AscCommon.align_Left;
|
|
};
|
|
|
|
|
|
StringRender.prototype.addClipRect = function(x, y, w, h) {
|
|
this.clipRect.x = x;
|
|
this.clipRect.y = y;
|
|
this.clipRect.w = w;
|
|
this.clipRect.h = h;
|
|
this.clipRect.use = true;
|
|
};
|
|
|
|
StringRender.prototype.removeClipRect = function() {
|
|
this.clipRect.use = false;
|
|
};
|
|
//------------------------------------------------------------export---------------------------------------------------
|
|
window['AscCommonExcel'] = window['AscCommonExcel'] || {};
|
|
window["AscCommonExcel"].StringRender = StringRender;
|
|
|
|
|
|
function TableCellDrawState(stringRender) {
|
|
this.stringRender = stringRender;
|
|
this.bidiFlow = new AscWord.BidiFlow(this);
|
|
this.drawingCtx = this.stringRender.drawingCtx;
|
|
this.x = 0;
|
|
this.y = 0;
|
|
this.baseY = 0;
|
|
this.zoom = 1;
|
|
this.ppiy = 96;
|
|
this.currentFont = null;
|
|
this.currentColor = null;
|
|
this.textColor = null;
|
|
this.angle = 0;
|
|
this.currentLine = null;
|
|
this.startIdx = 0;
|
|
}
|
|
|
|
|
|
TableCellDrawState.prototype.endLine = function() {
|
|
this.bidiFlow.end();
|
|
};
|
|
TableCellDrawState.prototype.getBidiType = function(char, charProp) {
|
|
if (charProp && charProp.nl) {
|
|
return AscBidi.TYPE.B;
|
|
}
|
|
if (this.stringRender.codesHypSp[char]) {
|
|
return AscBidi.TYPE.WS;
|
|
}
|
|
return AscBidi.getType(char);
|
|
};
|
|
TableCellDrawState.prototype.beginFragment = function(begin, end, prop) {
|
|
let i = begin;
|
|
while (i < end) {
|
|
let charProp = this.stringRender.charProps[i];
|
|
if (charProp && charProp.skip) {
|
|
i++;
|
|
continue;
|
|
}
|
|
let char = this.stringRender.chars[i];
|
|
let bidiType = this.getBidiType(char, charProp);
|
|
this.stringRender._setFont(this.drawingCtx, prop.font);
|
|
|
|
//todo: implement the stack of states in DrawingContext and remove this check
|
|
let textColor = prop.c || this.stringRender.textColor;
|
|
let _r = textColor.getR();
|
|
let _g = textColor.getG();
|
|
let _b = textColor.getB();
|
|
let _a = textColor.getA();
|
|
let setColor = true;
|
|
if (this.drawingCtx.fillColor && this.drawingCtx.fillColor.isEqual(_r, _g, _b, _a)) {
|
|
setColor = false;
|
|
}
|
|
if (setColor || window["IS_NATIVE_EDITOR"]) {
|
|
this.drawingCtx.setFillStyle(textColor);
|
|
}
|
|
/////
|
|
this.bidiFlow.add({
|
|
charIndex: i,
|
|
charProp: charProp,
|
|
fragmentProp: prop
|
|
}, bidiType);
|
|
i++;
|
|
}
|
|
};
|
|
|
|
TableCellDrawState.prototype.handleBidiFlow = function(data, direction) {
|
|
let charIndex = data.charIndex;
|
|
let width = this.stringRender.charWidths[charIndex];
|
|
let cr = this.stringRender.clipRect;
|
|
if (cr.use) {
|
|
if (cr.x > this.x + width || cr.x + cr.w < this.x) {
|
|
this.x += width;
|
|
return;
|
|
}
|
|
}
|
|
let charProp = data.charProp;
|
|
let char = this.stringRender.chars[charIndex];
|
|
let grapheme = charProp ? charProp.grapheme : AscFonts.NO_GRAPHEME;
|
|
|
|
if (direction === AscBidi.DIRECTION.R && AscBidi.isPairedBracket(char)) {
|
|
if (grapheme !== AscFonts.NO_GRAPHEME) {
|
|
grapheme = AscBidi.getPairedBracketGrapheme(grapheme);
|
|
}
|
|
}
|
|
|
|
let fontSize = data.fragmentProp && data.fragmentProp.font ? data.fragmentProp.font.getSize() : 10;
|
|
let y = this.y;
|
|
|
|
if (grapheme !== AscFonts.NO_GRAPHEME) {
|
|
AscFonts.DrawGrapheme(grapheme, this.drawingCtx, this.x, y, fontSize, this.ppiy / 25.4);
|
|
}
|
|
|
|
this.x += width;
|
|
};
|
|
|
|
TableCellDrawState.prototype.beginLine = function(line, x, y) {
|
|
this.currentLine = line;
|
|
this.x = x;
|
|
this.y = y;
|
|
this.baseY = y;
|
|
|
|
this.bidiFlow.begin(this.getMainDirection() === AscBidi.DIRECTION_FLAG.RTL);
|
|
};
|
|
|
|
|
|
|
|
TableCellDrawState.prototype.reset = function(drawingCtx, textColor, flags, angle) {
|
|
this.drawingCtx = drawingCtx || this.stringRender.drawingCtx;
|
|
this.x = 0;
|
|
this.y = 0;
|
|
this.baseY = 0;
|
|
this.currentFont = null;
|
|
this.currentColor = null;
|
|
this.currentLine = null;
|
|
this.startIdx = 0;
|
|
this.textColor = textColor || null;
|
|
this.angle = angle || 0;
|
|
this.zoom = this.drawingCtx.getZoom();
|
|
this.ppiy = this.drawingCtx.getPPIY();
|
|
};
|
|
TableCellDrawState.prototype.getMainDirection = function() {
|
|
let readingOrder = this.stringRender.flags ? this.stringRender.flags.getReadingOrder() : null;
|
|
if (readingOrder === Asc.c_oReadingOrderTypes.LTR) {
|
|
return AscBidi.DIRECTION_FLAG.LTR;
|
|
} else if (readingOrder === Asc.c_oReadingOrderTypes.RTL) {
|
|
return AscBidi.DIRECTION_FLAG.RTL;
|
|
}
|
|
for (let i = 0; i < this.stringRender.chars.length; ++i) {
|
|
let char = this.stringRender.chars[i];
|
|
let strongDir = AscCommon.getCharStrongDir(char);
|
|
if (strongDir !== null) {
|
|
return strongDir;
|
|
}
|
|
}
|
|
return AscBidi.DIRECTION_FLAG.LTR;
|
|
};
|
|
|
|
}
|
|
|
|
|
|
)(window);
|