blob: fd8902294b6a004ca50586f57aba644b6e03e5e7 [file] [log] [blame]
/*
* Copyright (C) Research In Motion Limited 2010-2012. All rights reserved.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public License
* along with this library; see the file COPYING.LIB. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301, USA.
*/
#include "config.h"
#include "SVGTextQuery.h"
#include "FloatConversion.h"
#include "InlineFlowBox.h"
#include "RenderBlockFlow.h"
#include "RenderInline.h"
#include "RenderSVGText.h"
#include "SVGInlineTextBox.h"
#include "VisiblePosition.h"
#include <wtf/MathExtras.h>
namespace WebCore {
// Base structure for callback user data
struct SVGTextQuery::Data {
Data()
: isVerticalText(false)
, processedCharacters(0)
, textRenderer(0)
, textBox(0)
{
}
bool isVerticalText;
unsigned processedCharacters;
RenderSVGInlineText* textRenderer;
const SVGInlineTextBox* textBox;
};
static inline InlineFlowBox* flowBoxForRenderer(RenderObject* renderer)
{
if (!renderer)
return nullptr;
if (is<RenderBlockFlow>(*renderer)) {
// If we're given a block element, it has to be a RenderSVGText.
ASSERT(is<RenderSVGText>(*renderer));
RenderBlockFlow& renderBlock = downcast<RenderBlockFlow>(*renderer);
// RenderSVGText only ever contains a single line box.
auto flowBox = renderBlock.firstRootBox();
ASSERT(flowBox == renderBlock.lastRootBox());
return flowBox;
}
if (is<RenderInline>(*renderer)) {
// We're given a RenderSVGInline or objects that derive from it (RenderSVGTSpan / RenderSVGTextPath)
RenderInline& renderInline = downcast<RenderInline>(*renderer);
// RenderSVGInline only ever contains a single line box.
InlineFlowBox* flowBox = renderInline.firstLineBox();
ASSERT(flowBox == renderInline.lastLineBox());
return flowBox;
}
ASSERT_NOT_REACHED();
return nullptr;
}
SVGTextQuery::SVGTextQuery(RenderObject* renderer)
{
collectTextBoxesInFlowBox(flowBoxForRenderer(renderer));
}
void SVGTextQuery::collectTextBoxesInFlowBox(InlineFlowBox* flowBox)
{
if (!flowBox)
return;
for (InlineBox* child = flowBox->firstChild(); child; child = child->nextOnLine()) {
if (is<InlineFlowBox>(*child)) {
// Skip generated content.
if (!child->renderer().node())
continue;
collectTextBoxesInFlowBox(downcast<InlineFlowBox>(child));
continue;
}
if (is<SVGInlineTextBox>(*child))
m_textBoxes.append(downcast<SVGInlineTextBox>(child));
}
}
bool SVGTextQuery::executeQuery(Data* queryData, ProcessTextFragmentCallback fragmentCallback) const
{
ASSERT(!m_textBoxes.isEmpty());
unsigned processedCharacters = 0;
unsigned textBoxCount = m_textBoxes.size();
// Loop over all text boxes
for (unsigned textBoxPosition = 0; textBoxPosition < textBoxCount; ++textBoxPosition) {
queryData->textBox = m_textBoxes.at(textBoxPosition);
queryData->textRenderer = &queryData->textBox->renderer();
queryData->isVerticalText = queryData->textRenderer->style().isVerticalWritingMode();
const Vector<SVGTextFragment>& fragments = queryData->textBox->textFragments();
// Loop over all text fragments in this text box, firing a callback for each.
unsigned fragmentCount = fragments.size();
for (unsigned i = 0; i < fragmentCount; ++i) {
const SVGTextFragment& fragment = fragments.at(i);
if ((this->*fragmentCallback)(queryData, fragment))
return true;
processedCharacters += fragment.length;
}
queryData->processedCharacters = processedCharacters;
}
return false;
}
bool SVGTextQuery::mapStartEndPositionsIntoFragmentCoordinates(Data* queryData, const SVGTextFragment& fragment, unsigned& startPosition, unsigned& endPosition) const
{
// Reuse the same logic used for text selection & painting, to map our query start/length into start/endPositions of the current text fragment.
ASSERT(startPosition >= queryData->processedCharacters);
ASSERT(endPosition >= queryData->processedCharacters);
startPosition -= queryData->processedCharacters;
endPosition -= queryData->processedCharacters;
if (startPosition >= endPosition)
return false;
modifyStartEndPositionsRespectingLigatures(queryData, startPosition, endPosition);
if (!queryData->textBox->mapStartEndPositionsIntoFragmentCoordinates(fragment, startPosition, endPosition))
return false;
ASSERT_WITH_SECURITY_IMPLICATION(startPosition < endPosition);
return true;
}
void SVGTextQuery::modifyStartEndPositionsRespectingLigatures(Data* queryData, unsigned& startPosition, unsigned& endPosition) const
{
SVGTextLayoutAttributes* layoutAttributes = queryData->textRenderer->layoutAttributes();
Vector<SVGTextMetrics>& textMetricsValues = layoutAttributes->textMetricsValues();
unsigned boxStart = queryData->textBox->start();
unsigned boxLength = queryData->textBox->len();
unsigned textMetricsOffset = 0;
unsigned textMetricsSize = textMetricsValues.size();
unsigned positionOffset = 0;
unsigned positionSize = layoutAttributes->context().text().length();
bool alterStartPosition = true;
bool alterEndPosition = true;
std::optional<unsigned> lastPositionOffset;
for (; textMetricsOffset < textMetricsSize && positionOffset < positionSize; ++textMetricsOffset) {
SVGTextMetrics& metrics = textMetricsValues[textMetricsOffset];
// Advance to text box start location.
if (positionOffset < boxStart) {
positionOffset += metrics.length();
continue;
}
// Stop if we've finished processing this text box.
if (positionOffset >= boxStart + boxLength)
break;
// If the start position maps to a character in the metrics list, we don't need to modify it.
if (startPosition == positionOffset)
alterStartPosition = false;
// If the start position maps to a character in the metrics list, we don't need to modify it.
if (endPosition == positionOffset)
alterEndPosition = false;
// Detect ligatures.
if (lastPositionOffset && lastPositionOffset.value() - positionOffset > 1) {
if (alterStartPosition && startPosition > lastPositionOffset.value() && startPosition < positionOffset) {
startPosition = lastPositionOffset.value();
alterStartPosition = false;
}
if (alterEndPosition && endPosition > lastPositionOffset.value() && endPosition < positionOffset) {
endPosition = positionOffset;
alterEndPosition = false;
}
}
if (!alterStartPosition && !alterEndPosition)
break;
lastPositionOffset = positionOffset;
positionOffset += metrics.length();
}
if (!alterStartPosition && !alterEndPosition)
return;
if (lastPositionOffset && lastPositionOffset.value() - positionOffset > 1) {
if (alterStartPosition && startPosition > lastPositionOffset.value() && startPosition < positionOffset)
startPosition = lastPositionOffset.value();
if (alterEndPosition && endPosition > lastPositionOffset.value() && endPosition < positionOffset)
endPosition = positionOffset;
}
}
// numberOfCharacters() implementation
bool SVGTextQuery::numberOfCharactersCallback(Data*, const SVGTextFragment&) const
{
// no-op
return false;
}
unsigned SVGTextQuery::numberOfCharacters() const
{
if (m_textBoxes.isEmpty())
return 0;
Data data;
executeQuery(&data, &SVGTextQuery::numberOfCharactersCallback);
return data.processedCharacters;
}
// textLength() implementation
struct TextLengthData : SVGTextQuery::Data {
TextLengthData()
: textLength(0)
{
}
float textLength;
};
bool SVGTextQuery::textLengthCallback(Data* queryData, const SVGTextFragment& fragment) const
{
TextLengthData* data = static_cast<TextLengthData*>(queryData);
data->textLength += queryData->isVerticalText ? fragment.height : fragment.width;
return false;
}
float SVGTextQuery::textLength() const
{
if (m_textBoxes.isEmpty())
return 0;
TextLengthData data;
executeQuery(&data, &SVGTextQuery::textLengthCallback);
return data.textLength;
}
// subStringLength() implementation
struct SubStringLengthData : SVGTextQuery::Data {
SubStringLengthData(unsigned queryStartPosition, unsigned queryLength)
: startPosition(queryStartPosition)
, length(queryLength)
, subStringLength(0)
{
}
unsigned startPosition;
unsigned length;
float subStringLength;
};
bool SVGTextQuery::subStringLengthCallback(Data* queryData, const SVGTextFragment& fragment) const
{
SubStringLengthData* data = static_cast<SubStringLengthData*>(queryData);
unsigned startPosition = data->startPosition;
unsigned endPosition = startPosition + data->length;
if (!mapStartEndPositionsIntoFragmentCoordinates(queryData, fragment, startPosition, endPosition))
return false;
SVGTextMetrics metrics = SVGTextMetrics::measureCharacterRange(*queryData->textRenderer, fragment.characterOffset + startPosition, endPosition - startPosition);
data->subStringLength += queryData->isVerticalText ? metrics.height() : metrics.width();
return false;
}
float SVGTextQuery::subStringLength(unsigned startPosition, unsigned length) const
{
if (m_textBoxes.isEmpty())
return 0;
SubStringLengthData data(startPosition, length);
executeQuery(&data, &SVGTextQuery::subStringLengthCallback);
return data.subStringLength;
}
// startPositionOfCharacter() implementation
struct StartPositionOfCharacterData : SVGTextQuery::Data {
StartPositionOfCharacterData(unsigned queryPosition)
: position(queryPosition)
{
}
unsigned position;
FloatPoint startPosition;
};
bool SVGTextQuery::startPositionOfCharacterCallback(Data* queryData, const SVGTextFragment& fragment) const
{
StartPositionOfCharacterData* data = static_cast<StartPositionOfCharacterData*>(queryData);
unsigned startPosition = data->position;
unsigned endPosition = startPosition + 1;
if (!mapStartEndPositionsIntoFragmentCoordinates(queryData, fragment, startPosition, endPosition))
return false;
data->startPosition = FloatPoint(fragment.x, fragment.y);
if (startPosition) {
SVGTextMetrics metrics = SVGTextMetrics::measureCharacterRange(*queryData->textRenderer, fragment.characterOffset, startPosition);
if (queryData->isVerticalText)
data->startPosition.move(0, metrics.height());
else
data->startPosition.move(metrics.width(), 0);
}
AffineTransform fragmentTransform;
fragment.buildFragmentTransform(fragmentTransform, SVGTextFragment::TransformIgnoringTextLength);
if (fragmentTransform.isIdentity())
return true;
data->startPosition = fragmentTransform.mapPoint(data->startPosition);
return true;
}
FloatPoint SVGTextQuery::startPositionOfCharacter(unsigned position) const
{
if (m_textBoxes.isEmpty())
return { };
StartPositionOfCharacterData data(position);
executeQuery(&data, &SVGTextQuery::startPositionOfCharacterCallback);
return data.startPosition;
}
// endPositionOfCharacter() implementation
struct EndPositionOfCharacterData : SVGTextQuery::Data {
EndPositionOfCharacterData(unsigned queryPosition)
: position(queryPosition)
{
}
unsigned position;
FloatPoint endPosition;
};
bool SVGTextQuery::endPositionOfCharacterCallback(Data* queryData, const SVGTextFragment& fragment) const
{
EndPositionOfCharacterData* data = static_cast<EndPositionOfCharacterData*>(queryData);
unsigned startPosition = data->position;
unsigned endPosition = startPosition + 1;
if (!mapStartEndPositionsIntoFragmentCoordinates(queryData, fragment, startPosition, endPosition))
return false;
data->endPosition = FloatPoint(fragment.x, fragment.y);
SVGTextMetrics metrics = SVGTextMetrics::measureCharacterRange(*queryData->textRenderer, fragment.characterOffset, startPosition + 1);
if (queryData->isVerticalText)
data->endPosition.move(0, metrics.height());
else
data->endPosition.move(metrics.width(), 0);
AffineTransform fragmentTransform;
fragment.buildFragmentTransform(fragmentTransform, SVGTextFragment::TransformIgnoringTextLength);
if (fragmentTransform.isIdentity())
return true;
data->endPosition = fragmentTransform.mapPoint(data->endPosition);
return true;
}
FloatPoint SVGTextQuery::endPositionOfCharacter(unsigned position) const
{
if (m_textBoxes.isEmpty())
return { };
EndPositionOfCharacterData data(position);
executeQuery(&data, &SVGTextQuery::endPositionOfCharacterCallback);
return data.endPosition;
}
// rotationOfCharacter() implementation
struct RotationOfCharacterData : SVGTextQuery::Data {
RotationOfCharacterData(unsigned queryPosition)
: position(queryPosition)
, rotation(0)
{
}
unsigned position;
float rotation;
};
bool SVGTextQuery::rotationOfCharacterCallback(Data* queryData, const SVGTextFragment& fragment) const
{
RotationOfCharacterData* data = static_cast<RotationOfCharacterData*>(queryData);
unsigned startPosition = data->position;
unsigned endPosition = startPosition + 1;
if (!mapStartEndPositionsIntoFragmentCoordinates(queryData, fragment, startPosition, endPosition))
return false;
AffineTransform fragmentTransform;
fragment.buildFragmentTransform(fragmentTransform, SVGTextFragment::TransformIgnoringTextLength);
if (fragmentTransform.isIdentity())
data->rotation = 0;
else {
fragmentTransform.scale(1 / fragmentTransform.xScale(), 1 / fragmentTransform.yScale());
data->rotation = narrowPrecisionToFloat(rad2deg(atan2(fragmentTransform.b(), fragmentTransform.a())));
}
return true;
}
float SVGTextQuery::rotationOfCharacter(unsigned position) const
{
if (m_textBoxes.isEmpty())
return 0;
RotationOfCharacterData data(position);
executeQuery(&data, &SVGTextQuery::rotationOfCharacterCallback);
return data.rotation;
}
// extentOfCharacter() implementation
struct ExtentOfCharacterData : SVGTextQuery::Data {
ExtentOfCharacterData(unsigned queryPosition)
: position(queryPosition)
{
}
unsigned position;
FloatRect extent;
};
static inline void calculateGlyphBoundaries(SVGTextQuery::Data* queryData, const SVGTextFragment& fragment, unsigned startPosition, FloatRect& extent)
{
float scalingFactor = queryData->textRenderer->scalingFactor();
ASSERT(scalingFactor);
extent.setLocation(FloatPoint(fragment.x, fragment.y - queryData->textRenderer->scaledFont().fontMetrics().floatAscent() / scalingFactor));
if (startPosition) {
SVGTextMetrics metrics = SVGTextMetrics::measureCharacterRange(*queryData->textRenderer, fragment.characterOffset, startPosition);
if (queryData->isVerticalText)
extent.move(0, metrics.height());
else
extent.move(metrics.width(), 0);
}
SVGTextMetrics metrics = SVGTextMetrics::measureCharacterRange(*queryData->textRenderer, fragment.characterOffset + startPosition, 1);
extent.setSize(FloatSize(metrics.width(), metrics.height()));
AffineTransform fragmentTransform;
fragment.buildFragmentTransform(fragmentTransform, SVGTextFragment::TransformIgnoringTextLength);
if (fragmentTransform.isIdentity())
return;
extent = fragmentTransform.mapRect(extent);
}
bool SVGTextQuery::extentOfCharacterCallback(Data* queryData, const SVGTextFragment& fragment) const
{
ExtentOfCharacterData* data = static_cast<ExtentOfCharacterData*>(queryData);
unsigned startPosition = data->position;
unsigned endPosition = startPosition + 1;
if (!mapStartEndPositionsIntoFragmentCoordinates(queryData, fragment, startPosition, endPosition))
return false;
calculateGlyphBoundaries(queryData, fragment, startPosition, data->extent);
return true;
}
FloatRect SVGTextQuery::extentOfCharacter(unsigned position) const
{
if (m_textBoxes.isEmpty())
return FloatRect();
ExtentOfCharacterData data(position);
executeQuery(&data, &SVGTextQuery::extentOfCharacterCallback);
return data.extent;
}
// characterNumberAtPosition() implementation
struct CharacterNumberAtPositionData : SVGTextQuery::Data {
CharacterNumberAtPositionData(const FloatPoint& queryPosition)
: position(queryPosition)
{
}
FloatPoint position;
};
bool SVGTextQuery::characterNumberAtPositionCallback(Data* queryData, const SVGTextFragment& fragment) const
{
CharacterNumberAtPositionData* data = static_cast<CharacterNumberAtPositionData*>(queryData);
FloatRect extent;
for (unsigned i = 0; i < fragment.length; ++i) {
unsigned startPosition = data->processedCharacters + i;
unsigned endPosition = startPosition + 1;
if (!mapStartEndPositionsIntoFragmentCoordinates(queryData, fragment, startPosition, endPosition))
continue;
calculateGlyphBoundaries(queryData, fragment, startPosition, extent);
if (extent.contains(data->position)) {
data->processedCharacters += i;
return true;
}
}
return false;
}
int SVGTextQuery::characterNumberAtPosition(const FloatPoint& position) const
{
if (m_textBoxes.isEmpty())
return -1;
CharacterNumberAtPositionData data(position);
if (!executeQuery(&data, &SVGTextQuery::characterNumberAtPositionCallback))
return -1;
return data.processedCharacters;
}
}