using namespace skia_private;
using namespace skglyph;
// -- GPU Text ------------------------------------------------------------------------------------- // Naming conventions // * drawMatrix - the CTM from the canvas. // * drawOrigin - the x, y location of the drawTextBlob call. // * positionMatrix - this is the combination of the drawMatrix and the drawOrigin: // positionMatrix = drawMatrix * TranslationMatrix(drawOrigin.x, drawOrigin.y); // // Note: // In order to transform Slugs, you need to set the fSupportBilerpFromGlyphAtlas on // GrContextOptions.
namespace sktext::gpu { // -- SubRunStreamTag ------------------------------------------------------------------------------ enum SubRun::SubRunStreamTag : int {
kBad = 0, // Make this 0 to line up with errors from readInt.
kDirectMaskStreamTag, #if !defined(SK_DISABLE_SDF_TEXT)
kSDFTStreamTag, #endif
kTransformMaskStreamTag,
kPathStreamTag,
kDrawableStreamTag,
kSubRunStreamTagCount,
};
} // namespace sktext::gpu
using MaskFormat = skgpu::MaskFormat;
using namespace sktext;
using namespace sktext::gpu;
// -- PathOpSubmitter ------------------------------------------------------------------------------ // PathOpSubmitter holds glyph ids until ready to draw. During drawing, the glyph ids are // converted to SkPaths. PathOpSubmitter can only be serialized when it is holding glyph ids; // it can only be serialized before submitDraws has been called. class PathOpSubmitter {
public:
PathOpSubmitter() = delete;
PathOpSubmitter(const PathOpSubmitter&) = delete; const PathOpSubmitter& operator=(const PathOpSubmitter&) = delete;
PathOpSubmitter(PathOpSubmitter&& that) // Transfer ownership of fIDsOrPaths from that to this.
: fIDsOrPaths{std::exchange( const_cast<SkSpan<IDOrPath>&>(that.fIDsOrPaths), SkSpan<IDOrPath>{})}
, fPositions{that.fPositions}
, fStrikeToSourceScale{that.fStrikeToSourceScale}
, fIsAntiAliased{that.fIsAntiAliased}
, fStrikePromise{std::move(that.fStrikePromise)} {}
PathOpSubmitter& operator=(PathOpSubmitter&& that) {
this->~PathOpSubmitter(); new (this) PathOpSubmitter{std::move(that)}; return *this;
}
PathOpSubmitter(bool isAntiAliased,
SkScalar strikeToSourceScale,
SkSpan<SkPoint> positions,
SkSpan<IDOrPath> idsOrPaths,
SkStrikePromise&& strikePromise);
// submitDraws is not thread safe. It only occurs the single thread drawing portion of the GPU // rendering. void submitDraws(SkCanvas*,
SkPoint drawOrigin, const SkPaint& paint) const;
private: // When PathOpSubmitter is created only the glyphIDs are needed, during the submitDraws call, // the glyphIDs are converted to SkPaths. const SkSpan<IDOrPath> fIDsOrPaths; const SkSpan<const SkPoint> fPositions; const SkScalar fStrikeToSourceScale; constbool fIsAntiAliased;
// Remember, we stored an int for glyph id. if (!buffer.validateCanReadN<int>(glyphCount)) { return std::nullopt; } auto idsOrPaths = SkSpan(alloc->makeUniqueArray<IDOrPath>(glyphCount).release(), glyphCount); for (auto& idOrPath : idsOrPaths) {
idOrPath.fGlyphID = SkTo<SkGlyphID>(buffer.readInt());
}
PathOpSubmitter::~PathOpSubmitter() { // If we have converted glyph IDs to paths, then clean up the SkPaths. if (fPathsAreCreated) { for (auto& idOrPath : fIDsOrPaths) {
idOrPath.fPath.~SkPath();
}
}
}
void
PathOpSubmitter::submitDraws(SkCanvas* canvas, SkPoint drawOrigin, const SkPaint& paint) const { // Convert the glyph IDs to paths if it hasn't been done yet. This is thread safe.
fConvertIDsToPaths([&]() { if (SkStrike* strike = fStrikePromise.strike()) {
strike->glyphIDsToPaths(fIDsOrPaths);
// Drop ref to strike so that it can be purged from the cache if needed.
fStrikePromise.resetStrike();
fPathsAreCreated = true;
}
});
// Calculate the matrix that maps the path glyphs from their size in the strike to // the graphics source space.
SkMatrix strikeToSource = SkMatrix::Scale(fStrikeToSourceScale, fStrikeToSourceScale);
strikeToSource.postTranslate(drawOrigin.x(), drawOrigin.y());
// If there are shaders, non-blur mask filters or styles, the path must be scaled into source // space independently of the CTM. This allows the CTM to be correct for the different effects.
SkStrokeRec style(runPaint); bool needsExactCTM = runPaint.getShader()
|| runPaint.getPathEffect()
|| (!style.isFillStyle() && !style.isHairlineStyle())
|| (maskFilter != nullptr && !maskFilter->asABlur(nullptr)); if (!needsExactCTM) {
SkMaskFilterBase::BlurRec blurRec;
// If there is a blur mask filter, then sigma needs to be adjusted to account for the // scaling of fStrikeToSourceScale. if (maskFilter != nullptr && maskFilter->asABlur(&blurRec)) {
runPaint.setMaskFilter(
SkMaskFilter::MakeBlur(blurRec.fStyle, blurRec.fSigma / fStrikeToSourceScale));
} for (auto [idOrPath, pos] : SkMakeZip(fIDsOrPaths, fPositions)) { // Transform the glyph to source space.
SkMatrix pathMatrix = strikeToSource;
pathMatrix.postTranslate(pos.x(), pos.y());
SkAutoCanvasRestore acr(canvas, true);
canvas->concat(pathMatrix);
canvas->drawPath(idOrPath.fPath, runPaint);
}
} else { // Transform the path to device because the deviceMatrix must be unchanged to // draw effect, filter or shader paths. for (auto [idOrPath, pos] : SkMakeZip(fIDsOrPaths, fPositions)) { // Transform the glyph to source space.
SkMatrix pathMatrix = strikeToSource;
pathMatrix.postTranslate(pos.x(), pos.y());
private: const SkScalar fStrikeToSourceScale; const SkSpan<SkPoint> fPositions; const SkSpan<IDOrDrawable> fIDsOrDrawables; // When the promise is converted to a strike it acts as the ref on the strike to keep the // SkDrawable data alive. mutable SkStrikePromise fStrikePromise; mutable SkOnce fConvertIDsToDrawables;
};
int DrawableOpSubmitter::unflattenSize() const { return fPositions.size_bytes() + fIDsOrDrawables.size_bytes();
}
if (!buffer.validateCanReadN<int>(glyphCount)) { return std::nullopt; } auto idsOrDrawables = alloc->makePODArray<IDOrDrawable>(glyphCount); for (int i = 0; i < SkToInt(glyphCount); ++i) { // Remember, we stored an int for glyph id.
idsOrDrawables[i].fGlyphID = SkTo<SkGlyphID>(buffer.readInt());
}
void
DrawableOpSubmitter::submitDraws(SkCanvas* canvas, SkPoint drawOrigin,const SkPaint& paint) const { // Convert glyph IDs to Drawables if it hasn't been done yet.
fConvertIDsToDrawables([&]() {
fStrikePromise.strike()->glyphIDsToDrawables(fIDsOrDrawables); // Do not call resetStrike() because the strike must remain owned to ensure the Drawable // data is not freed.
});
// Calculate the matrix that maps the path glyphs from their size in the strike to // the graphics source space.
SkMatrix strikeToSource = SkMatrix::Scale(fStrikeToSourceScale, fStrikeToSourceScale);
strikeToSource.postTranslate(drawOrigin.x(), drawOrigin.y());
// Transform the path to device because the deviceMatrix must be unchanged to // draw effect, filter or shader paths. for (auto [i, position] : SkMakeEnumerate(fPositions)) {
SkDrawable* drawable = fIDsOrDrawables[i].fDrawable;
if (drawable == nullptr) { // This better be pinned to keep the drawable data alive.
fStrikePromise.strike()->verifyPinnedStrike();
SkDEBUGFAIL("Drawable should not be nullptr."); continue;
}
// Transform the glyph to source space.
SkMatrix pathMatrix = strikeToSource;
pathMatrix.postTranslate(position.x(), position.y());
GlyphParams glyphParams() const override { // Since this is non-SDF, isAA will be ignored so we just pass true return { /*isSDF=*/false, fVertexFiller.isLCD(), /*isAA=*/true };
}
bool canReuse(const SkPaint& paint, const SkMatrix& positionMatrix) const override { // If we are not scaling the cache entry to be larger, than a cache with smaller glyphs may // be better. return fIsBigEnough;
}
GlyphParams glyphParams() const override { // Since this is non-SDF, isAA will be ignored so we just pass true return { /*isSDF=*/false, fVertexFiller.isLCD(), /*isAA=*/true };
}
static SubRunOwner Make(SkZip<const SkPackedGlyphID, const SkPoint> accepted, const SkFont& runFont,
SkStrikePromise&& strikePromise, const SkMatrix& creationMatrix,
SkRect creationBounds, const SDFTMatrixRange& matrixRange,
SubRunAllocator* alloc) { // NOTE: The MaskFormat is hard coded to kA8 here. This means that when isLCD() is called // from VertexFiller, it always returns false. However, VertexFiller has no other behavior // that depends on this function, and drawAtlasSubrun instead determines LCD based on the // fUseLCDText member variable. If this ever changes, VertexFiller will need to take an // additional isLCD argument on the constructor. auto vertexFiller = VertexFiller::Make(MaskFormat::kA8,
creationMatrix,
creationBounds,
get_positions(accepted),
alloc,
kIsTransformed);
// NOTE: When creating the SubRun, only the runFont edging determines LCD usage. This is // because the upstream preparation in SubRunContainer::MakeInAlloc is identical between // LCD and non-LCD. Instead LCD is set here, so that when the subRun is consumed in // drawAtlasSubRun, the SDFLCD renderer is chosen. return alloc->makeUnique<SDFTSubRun>(
runFont.getEdging() == SkFont::Edging::kSubpixelAntiAlias,
has_some_antialiasing(runFont),
matrixRange,
std::move(vertexFiller),
std::move(glyphVector));
}
static SubRunOwner MakeFromBuffer(SkReadBuffer& buffer,
SubRunAllocator* alloc, const SkStrikeClient* client) { int useLCD = buffer.readInt(); int isAntiAliased = buffer.readInt();
SDFTMatrixRange matrixRange = SDFTMatrixRange::MakeFromBuffer(buffer); auto vertexFiller = VertexFiller::MakeFromBuffer(buffer, alloc); if (!buffer.validate(vertexFiller.has_value())) { return nullptr; } if (!buffer.validate(vertexFiller.value().maskFormat() == MaskFormat::kA8)) { return nullptr;
} auto glyphVector = GlyphVector::MakeFromBuffer(buffer, client, alloc); if (!buffer.validate(glyphVector.has_value())) { return nullptr; } if (!buffer.validate(glyphVector->glyphCount() == vertexFiller->count())) { return nullptr;
} return alloc->makeUnique<SDFTSubRun>(useLCD,
isAntiAliased,
matrixRange,
std::move(*vertexFiller),
std::move(*glyphVector));
}
auto maskSpan = accepted.get<2>();
MaskFormat format = FormatFromSkGlyph(maskSpan[0]);
size_t startIndex = 0; for (size_t i = 1; i < accepted.size(); i++) {
MaskFormat nextFormat = FormatFromSkGlyph(maskSpan[i]); if (format != nextFormat) { auto interval = accepted.subspan(startIndex, i - startIndex); // Only pass the packed glyph ids and positions. auto glyphsWithSameFormat = SkMakeZip(interval.get<0>(), interval.get<1>()); // Take a ref on the strike. This should rarely happen.
addSingleMaskFormat(glyphsWithSameFormat, format);
format = nextFormat;
startIndex = i;
}
} auto interval = accepted.last(accepted.size() - startIndex); auto glyphsWithSameFormat = SkMakeZip(interval.get<0>(), interval.get<1>());
addSingleMaskFormat(glyphsWithSameFormat, format);
}
} // anonymous namespace
int SubRunContainer::AllocSizeHintFromBuffer(SkReadBuffer& buffer) { int subRunsSizeHint = buffer.readInt();
// Since the hint doesn't affect correctness, if it looks fishy just pick a reasonable // value. if (subRunsSizeHint < 0 || (1 << 16) < subRunsSizeHint) {
subRunsSizeHint = 128;
} return subRunsSizeHint;
}
int subRunCount = buffer.readInt(); if (!buffer.validate(subRunCount > 0)) { return nullptr; } for (int i = 0; i < subRunCount; ++i) { auto subRunOwner = SubRun::MakeFromBuffer(buffer, alloc, client); if (!buffer.validate(subRunOwner != nullptr)) { return nullptr; } if (subRunOwner != nullptr) {
container->fSubRuns.append(std::move(subRunOwner));
}
} return container;
}
size_t SubRunContainer::EstimateAllocSize(const GlyphRunList& glyphRunList) { // The difference in alignment from the per-glyph data to the SubRun;
constexpr size_t alignDiff = alignof(DirectMaskSubRun) - alignof(SkPoint);
constexpr size_t vertexDataToSubRunPadding = alignDiff > 0 ? alignDiff : 0;
size_t totalGlyphCount = glyphRunList.totalGlyphCount(); // This is optimized for DirectMaskSubRun which is by far the most common case. return totalGlyphCount * sizeof(SkPoint)
+ GlyphVector::Size(totalGlyphCount)
+ glyphRunList.runCount() * (sizeof(DirectMaskSubRun) + vertexDataToSubRunPadding)
+ sizeof(SubRunContainer);
}
// Build up the mapping from source space to device space. Add the rounding constant // halfSampleFreq, so we just need to floor to get the device result.
SkMatrix positionMatrixWithRounding = positionMatrix;
positionMatrixWithRounding.postTranslate(halfSampleFreq.x(), halfSampleFreq.y());
int acceptedSize = 0,
rejectedSize = 0;
SkGlyphRect boundingRect = skglyph::empty_rect();
StrikeMutationMonitor m{strike}; for (auto [glyphID, pos] : source) { if (!SkIsFinite(pos.x(), pos.y())) { continue;
}
#if !defined(SK_DISABLE_SDF_TEXT) static std::tuple<SkStrikeSpec, SkScalar, sktext::gpu::SDFTMatrixRange>
make_sdft_strike_spec(const SkFont& font, const SkPaint& paint, const SkSurfaceProps& surfaceProps, const SkMatrix& deviceMatrix, const SkPoint& textLocation, const sktext::gpu::SubRunControl& control) { // Add filter to the paint which creates the SDFT data for A8 masks.
SkPaint dfPaint{paint};
dfPaint.setMaskFilter(sktext::gpu::SDFMaskFilter::Make());
auto [dfFont, strikeToSourceScale, matrixRange] = control.getSDFFont(font, deviceMatrix,
textLocation);
// Adjust the stroke width by the scale factor for drawing the SDFT.
dfPaint.setStrokeWidth(paint.getStrokeWidth() / strikeToSourceScale);
// Check for dashing and adjust the intervals. if (SkPathEffect* pathEffect = paint.getPathEffect(); pathEffect != nullptr) { if (auto info = as_PEB(pathEffect)->asADash()) {
SkSpan<const SkScalar> src = info->fIntervals;
SkASSERT(src.size() > 1); // Allocate the intervals.
std::vector<SkScalar> scaledIntervals(src.size()); for (size_t i = 0; i < src.size(); ++i) {
scaledIntervals[i] = src[i] / strikeToSourceScale;
} auto scaledDashes = SkDashPathEffect::Make(scaledIntervals,
info->fPhase / strikeToSourceScale);
dfPaint.setPathEffect(scaledDashes);
}
}
// Fake-gamma and subpixel antialiasing are applied in the shader, so we ignore the // passed-in scaler context flags. (It's only used when we fall-back to bitmap text).
SkScalerContextFlags flags = SkScalerContextFlags::kNone;
SkStrikeSpec strikeSpec = SkStrikeSpec::MakeMask(dfFont, dfPaint, surfaceProps, flags,
SkMatrix::I());
SubRunContainerOwner container = alloc->makeUnique<SubRunContainer>(positionMatrix); // If there is no SubRunControl description ignore all SubRuns. if (strikeDeviceInfo.fSubRunControl == nullptr) { return container;
}
// TODO: hoist the buffer structure to the GlyphRunBuilder. The buffer structure here is // still begin tuned, and this is expected to be slower until tuned. constint maxGlyphRunSize = glyphRunList.maxGlyphRunSize();
// Handle all the runs in the glyphRunList for (auto& glyphRun : glyphRunList) {
SkZip<const SkGlyphID, const SkPoint> source = glyphRun.source(); const SkFont& runFont = glyphRun.font();
const SkScalar approximateDeviceTextSize = // Since the positionMatrix has the origin prepended, use the plain // sourceBounds from above.
SkFontPriv::ApproximateTransformedTextSize(runFont, positionMatrix,
glyphRunListLocation);
// Atlas mask cases - SDFT and direct mask // Only consider using direct or SDFT drawing if not drawing hairlines and not too big. if ((runPaint.getStyle() != SkPaint::kStroke_Style || runPaint.getStrokeWidth() != 0) &&
approximateDeviceTextSize < maxMaskSize) {
#if !defined(SK_DISABLE_SDF_TEXT) // SDFT case if (subRunControl->isSDFT(approximateDeviceTextSize, runPaint, positionMatrix)) { // Process SDFT - This should be the .009% case. constauto& [strikeSpec, strikeToSourceScale, matrixRange] =
make_sdft_strike_spec(
runFont, runPaint, deviceProps, positionMatrix,
glyphRunListLocation, *subRunControl);
if (!SkScalarNearlyZero(strikeToSourceScale)) {
sk_sp<StrikeForGPU> strike = strikeSpec.findOrCreateScopedStrike(strikeCache);
// The creationMatrix needs to scale the strike data when inverted and // multiplied by the positionMatrix. The final CTM should be: // [positionMatrix][scale by strikeToSourceScale], // which should equal the following because of the transform during the vertex // calculation, // [positionMatrix][creationMatrix]^-1. // So, the creation matrix needs to be // [scale by 1/strikeToSourceScale].
SkMatrix creationMatrix =
SkMatrix::Scale(1.f/strikeToSourceScale, 1.f/strikeToSourceScale);
auto acceptedBuffer = SkMakeZip(acceptedPackedGlyphIDs, acceptedPositions); auto [accepted, rejected, creationBounds] = prepare_for_SDFT_drawing(
strike.get(), creationMatrix, source, acceptedBuffer, rejectedBuffer);
source = rejected;
if (creationBehavior == kAddSubRuns && !accepted.empty()) {
container->fSubRuns.append(SDFTSubRun::Make(
accepted,
runFont,
strike->strikePromise(),
creationMatrix,
creationBounds,
matrixRange,
alloc));
}
}
} #endif// !defined(SK_DISABLE_SDF_TEXT) // Mask filters with 3D format (e.g. EmbossMaskFilter) need to be drawn as a path // in order to apply the filter through the Canvas AutoLayer system. constbool needsAutoLayer = runPaint.getMaskFilter() &&
as_MFB(runPaint.getMaskFilter())->getFormat() == SkMask::k3D_Format; // Direct Mask case // Handle all the directly mapped mask subruns. if (!source.empty() && !positionMatrix.hasPerspective() && !needsAutoLayer) { // Process masks including ARGB - this should be the 99.99% case. // This will handle medium size emoji that are sharing the run with SDFT drawn text. // If things are too big they will be passed along to the drawing of last resort // below.
SkStrikeSpec strikeSpec = SkStrikeSpec::MakeMask(
runFont, runPaint, deviceProps, scalerContextFlags, positionMatrix);
// Drawable case // Handle all the drawable glyphs - usually large or perspective color glyphs. if (!source.empty()) { auto [strikeSpec, strikeToSourceScale] =
SkStrikeSpec::MakePath(runFont, runPaint, deviceProps, scalerContextFlags);
if (!SkScalarNearlyZero(strikeToSourceScale)) {
sk_sp<StrikeForGPU> strike = strikeSpec.findOrCreateScopedStrike(strikeCache);
auto acceptedBuffer = SkMakeZip(acceptedGlyphIDs, acceptedPositions); auto [accepted, rejected] =
prepare_for_drawable_drawing(strike.get(), source, acceptedBuffer, rejectedBuffer);
source = rejected;
// Path case // Handle path subruns. Mainly, large or large perspective glyphs with no color. if (!source.empty()) { auto [strikeSpec, strikeToSourceScale] =
SkStrikeSpec::MakePath(runFont, runPaint, deviceProps, scalerContextFlags);
if (!SkScalarNearlyZero(strikeToSourceScale)) {
sk_sp<StrikeForGPU> strike = strikeSpec.findOrCreateScopedStrike(strikeCache);
auto acceptedBuffer = SkMakeZip(acceptedGlyphIDs, acceptedPositions); auto [accepted, rejected] =
prepare_for_path_drawing(strike.get(), source, acceptedBuffer, rejectedBuffer);
source = rejected;
// Drawing of last resort case // Draw all the rest of the rejected glyphs from above. This scales out of the atlas to // the screen, so quality will suffer. This mainly handles large color or perspective // color not handled by Drawables. if (!source.empty() && !SkScalarNearlyZero(approximateDeviceTextSize)) { // Creation matrix will be changed below to meet the following criteria: // * No perspective - the font scaler and the strikes can't handle perspective masks. // * Fits atlas - creationMatrix will be conditioned so that the maximum glyph // dimension for this run will be < kMaxBilerpAtlasDimension.
SkMatrix creationMatrix = positionMatrix;
// Condition creationMatrix for perspective. if (creationMatrix.hasPerspective()) { // Find a scale factor that reduces pixelation caused by keystoning.
SkPoint center = glyphRunList.sourceBounds().center();
SkScalar maxAreaScale = SkMatrixPriv::DifferentialAreaScale(creationMatrix, center);
SkScalar perspectiveFactor = 1; if (SkIsFinite(maxAreaScale) && !SkScalarNearlyZero(maxAreaScale)) {
perspectiveFactor = SkScalarSqrt(maxAreaScale);
}
// Masks can not be created in perspective. Create a non-perspective font with a // scale that will support the perspective keystoning.
creationMatrix = SkMatrix::Scale(perspectiveFactor, perspectiveFactor);
}
// Reduce to make a one pixel border for the bilerp padding. staticconst constexpr SkScalar kMaxBilerpAtlasDimension =
SkGlyphDigest::kSkSideTooBigForAtlas - 2;
// Get the raw glyph IDs to simulate device drawing to figure the maximum device // dimension. const SkSpan<const SkGlyphID> glyphs = get_glyphIDs(source);
// maxGlyphDimension always returns an integer even though the return type is SkScalar. auto maxGlyphDimension = [&](const SkMatrix& m) { const SkStrikeSpec strikeSpec = SkStrikeSpec::MakeTransformMask(
runFont, runPaint, deviceProps, scalerContextFlags, m); const sk_sp<StrikeForGPU> gaugingStrike =
strikeSpec.findOrCreateScopedStrike(strikeCache); const SkScalar maxDimension =
find_maximum_glyph_dimension(gaugingStrike.get(), glyphs); // TODO: There is a problem where a small character (say .) and a large // character (say M) are in the same run. If the run is scaled to be very // large, then the M may return 0 because its dimensions are > 65535, but // the small character produces regular result because its largest dimension // is < 65535. This will create an improper scale factor causing the M to be // too large to fit in the atlas. Tracked by skbug.com/40044801. return maxDimension;
};
// Condition the creationMatrix so that glyphs fit in the atlas. for (SkScalar maxDimension = maxGlyphDimension(creationMatrix);
kMaxBilerpAtlasDimension < maxDimension;
maxDimension = maxGlyphDimension(creationMatrix))
{ // The SkScalerContext has a limit of 65536 maximum dimension. // reductionFactor will always be < 1 because // maxDimension > kMaxBilerpAtlasDimension, and because maxDimension will always // be an integer the reduction factor will always be at most 254 / 255.
SkScalar reductionFactor = kMaxBilerpAtlasDimension / maxDimension;
creationMatrix.postScale(reductionFactor, reductionFactor);
}
// Draw using the creationMatrix.
SkStrikeSpec strikeSpec = SkStrikeSpec::MakeTransformMask(
runFont, runPaint, deviceProps, scalerContextFlags, creationMatrix);
// Returns the empty span if there is a problem reading the positions.
SkSpan<SkPoint> MakePointsFromBuffer(SkReadBuffer& buffer, SubRunAllocator* alloc) {
uint32_t glyphCount = buffer.getArrayCount();
// Zero indicates a problem with serialization. if (!buffer.validate(glyphCount != 0)) { return {}; }
// Check that the count will not overflow the arena. if (!buffer.validate(glyphCount <= INT_MAX &&
BagOfBytes::WillCountFit<SkPoint>(glyphCount))) { return {}; }
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.