// -- 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,
};
// -- 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());
std::tuple<ClipMethod, SkIRect>
calculate_clip(const GrClip* clip, SkRect deviceBounds, SkRect glyphBounds) { if (clip == nullptr && !deviceBounds.intersects(glyphBounds)) { return {kClippedOut, SkIRect::MakeEmpty()};
} elseif (clip != nullptr) { switch (auto result = clip->preApply(glyphBounds, GrAA::kNo); result.fEffect) { case GrClip::Effect::kClippedOut: return {kClippedOut, SkIRect::MakeEmpty()}; case GrClip::Effect::kUnclipped: return {kUnclipped, SkIRect::MakeEmpty()}; case GrClip::Effect::kClipped: { if (result.fIsRRect && result.fRRect.isRect()) {
SkRect r = result.fRRect.rect(); if (result.fAA == GrAA::kNo || GrClip::IsPixelAligned(r)) {
SkIRect clipRect = SkIRect::MakeEmpty(); // Clip geometrically during onPrepare using clipRect.
r.round(&clipRect); if (clipRect.contains(glyphBounds)) { // If fully within the clip, signal no clipping using the empty rect. return {kUnclipped, SkIRect::MakeEmpty()};
} // Use the clipRect to clip the geometry. return {kGeometryClipped, clipRect};
} // Partial pixel clipped at this point. Have the GPU handle it.
}
} break;
}
} return {kGPUClipped, SkIRect::MakeEmpty()};
} #endif// defined(SK_GANESH) || defined(SK_USE_LEGACY_GANESH_TEXT_APIS)
// -- DirectMaskSubRun ----------------------------------------------------------------------------- class DirectMaskSubRun final : public SubRun, public AtlasSubRun { public:
DirectMaskSubRun(VertexFiller&& vertexFiller,
GlyphVector&& glyphs)
: fVertexFiller{std::move(vertexFiller)}
, fGlyphs{std::move(glyphs)} {}
auto [integerTranslate, subRunDeviceBounds] =
fVertexFiller.deviceRectAndCheckTransform(positionMatrix); if (subRunDeviceBounds.isEmpty()) { return {nullptr, nullptr};
} // Rect for optimized bounds clipping when doing an integer translate.
SkIRect geometricClipRect = SkIRect::MakeEmpty(); if (integerTranslate) { // We can clip geometrically using clipRect and ignore clip when an axis-aligned // rectangular non-AA clip is used. If clipRect is empty, and clip is nullptr, then // there is no clipping needed. const SkRect deviceBounds = SkRect::MakeWH(sdc->width(), sdc->height()); auto [clipMethod, clipRect] = calculate_clip(clip, deviceBounds, subRunDeviceBounds);
switch (clipMethod) { case kClippedOut: // Returning nullptr as op means skip this op. return {nullptr, nullptr}; case kUnclipped: case kGeometryClipped: // GPU clip is not needed.
clip = nullptr; break; case kGPUClipped: // Use th GPU clip; clipRect is ignored. break;
}
geometricClipRect = clipRect;
if (!geometricClipRect.isEmpty()) { SkASSERT(clip == nullptr); }
}
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;
}
// The regenerateAtlas method mutates fGlyphs. It should be called from onPrepare which must // be single threaded. mutable GlyphVector fGlyphs;
}; // class TransformedMaskSubRun
// The regenerateAtlas method mutates fGlyphs. It should be called from onPrepare which must // be single threaded. mutable GlyphVector fGlyphs;
}; // class SDFTSubRun
auto maskSpan = accepted.get<2>();
MaskFormat format = Glyph::FormatFromSkGlyph(maskSpan[0]);
size_t startIndex = 0; for (size_t i = 1; i < accepted.size(); i++) {
MaskFormat nextFormat = Glyph::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);
}
} // 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::GlyphVectorSize(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) {
SkPathEffectBase::DashInfo dashInfo; if (as_PEB(pathEffect)->asADash(&dashInfo) == SkPathEffectBase::DashType::kDash) { if (dashInfo.fCount > 0) { // Allocate the intervals.
std::vector<SkScalar> scaledIntervals(dashInfo.fCount);
dashInfo.fIntervals = scaledIntervals.data(); // Call again to get the interval data.
(void)as_PEB(pathEffect)->asADash(&dashInfo); for (SkScalar& interval : scaledIntervals) {
interval /= strikeToSourceScale;
} auto scaledDashes = SkDashPathEffect::Make(scaledIntervals.data(),
scaledIntervals.size(),
dashInfo.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;
// Direct Mask case // Handle all the directly mapped mask subruns. if (!source.empty() && !positionMatrix.hasPerspective()) { // 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 skia:13714. 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.