diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-21 11:44:51 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-21 11:44:51 +0000 |
commit | 9e3c08db40b8916968b9f30096c7be3f00ce9647 (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /gfx/thebes/gfxFontMissingGlyphs.cpp | |
parent | Initial commit. (diff) | |
download | thunderbird-upstream.tar.xz thunderbird-upstream.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'gfx/thebes/gfxFontMissingGlyphs.cpp')
-rw-r--r-- | gfx/thebes/gfxFontMissingGlyphs.cpp | 542 |
1 files changed, 542 insertions, 0 deletions
diff --git a/gfx/thebes/gfxFontMissingGlyphs.cpp b/gfx/thebes/gfxFontMissingGlyphs.cpp new file mode 100644 index 0000000000..949a71228c --- /dev/null +++ b/gfx/thebes/gfxFontMissingGlyphs.cpp @@ -0,0 +1,542 @@ +/* -*- Mode: C++; tab-width: 20; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "gfxFontMissingGlyphs.h" + +#include "gfxUtils.h" +#include "mozilla/gfx/2D.h" +#include "mozilla/gfx/Helpers.h" +#include "mozilla/gfx/PathHelpers.h" +#include "mozilla/LinkedList.h" +#include "mozilla/RefPtr.h" +#include "nsDeviceContext.h" +#include "nsLayoutUtils.h" +#include "TextDrawTarget.h" +#include "LayerUserData.h" + +using namespace mozilla; +using namespace mozilla::gfx; + +#define X 255 +static const uint8_t gMiniFontData[] = { + 0, X, 0, 0, X, 0, X, X, X, X, X, X, X, 0, X, X, X, X, X, X, X, X, X, X, + X, X, X, X, X, X, X, X, X, X, X, 0, 0, X, X, X, X, 0, X, X, X, X, X, X, + X, 0, X, 0, X, 0, 0, 0, X, 0, 0, X, X, 0, X, X, 0, 0, X, 0, 0, 0, 0, X, + X, 0, X, X, 0, X, X, 0, X, X, 0, X, X, 0, 0, X, 0, X, X, 0, 0, X, 0, 0, + X, 0, X, 0, X, 0, X, X, X, X, X, X, X, X, X, X, X, X, X, X, X, 0, 0, X, + X, X, X, X, X, X, X, X, X, X, X, 0, X, 0, 0, X, 0, X, X, X, X, X, X, X, + X, 0, X, 0, X, 0, X, 0, 0, 0, 0, X, 0, 0, X, 0, 0, X, X, 0, X, 0, 0, X, + X, 0, X, 0, 0, X, X, 0, X, X, 0, X, X, 0, 0, X, 0, X, X, 0, 0, X, 0, 0, + 0, X, 0, 0, X, 0, X, X, X, X, X, X, 0, 0, X, X, X, X, X, X, X, 0, 0, X, + X, X, X, 0, 0, X, X, 0, X, X, X, 0, 0, X, X, X, X, 0, X, X, X, X, 0, 0, +}; +#undef X + +/* Parameters that control the rendering of hexboxes. They look like this: + + BMP codepoints non-BMP codepoints + (U+0000 - U+FFFF) (U+10000 - U+10FFFF) + + +---------+ +-------------+ + | | | | + | HHH HHH | | HHH HHH HHH | + | HHH HHH | | HHH HHH HHH | + | HHH HHH | | HHH HHH HHH | + | HHH HHH | | HHH HHH HHH | + | HHH HHH | | HHH HHH HHH | + | | | | + | HHH HHH | | HHH HHH HHH | + | HHH HHH | | HHH HHH HHH | + | HHH HHH | | HHH HHH HHH | + | HHH HHH | | HHH HHH HHH | + | HHH HHH | | HHH HHH HHH | + | | | | + +---------+ +-------------+ +*/ + +/** Width of a minifont glyph (see above) */ +static const int MINIFONT_WIDTH = 3; +/** Height of a minifont glyph (see above) */ +static const int MINIFONT_HEIGHT = 5; +/** + * Gap between minifont glyphs (both horizontal and vertical) and also + * the minimum desired gap between the box border and the glyphs + */ +static const int HEX_CHAR_GAP = 1; +/** + * The amount of space between the vertical edge of the glyphbox and the + * box border. We make this nonzero so that when multiple missing glyphs + * occur consecutively there's a gap between their rendered boxes. + */ +static const int BOX_HORIZONTAL_INSET = 1; +/** The width of the border */ +static const int BOX_BORDER_WIDTH = 1; +/** + * The scaling factor for the border opacity; this is multiplied by the current + * opacity being used to draw the text. + */ +static const Float BOX_BORDER_OPACITY = 0.5; + +#ifndef MOZ_GFX_OPTIMIZE_MOBILE + +class GlyphAtlas { + public: + GlyphAtlas(RefPtr<SourceSurface>&& aSurface, const DeviceColor& aColor) + : mSurface(std::move(aSurface)), mColor(aColor) {} + ~GlyphAtlas() = default; + + already_AddRefed<SourceSurface> Surface() const { + RefPtr surface = mSurface; + return surface.forget(); + } + DeviceColor Color() const { return mColor; } + + private: + RefPtr<SourceSurface> mSurface; + DeviceColor mColor; +}; + +// This is an owning reference that we will manage via exchange() and +// explicit new/delete operations. +static std::atomic<GlyphAtlas*> gGlyphAtlas; + +/** + * Generates a new colored mini-font atlas from the mini-font mask. + */ +static GlyphAtlas* MakeGlyphAtlas(const DeviceColor& aColor) { + RefPtr<DrawTarget> glyphDrawTarget = + gfxPlatform::GetPlatform()->CreateOffscreenContentDrawTarget( + IntSize(MINIFONT_WIDTH * 16, MINIFONT_HEIGHT), + SurfaceFormat::B8G8R8A8); + if (!glyphDrawTarget) { + return nullptr; + } + RefPtr<SourceSurface> glyphMask = + glyphDrawTarget->CreateSourceSurfaceFromData( + const_cast<uint8_t*>(gMiniFontData), + IntSize(MINIFONT_WIDTH * 16, MINIFONT_HEIGHT), MINIFONT_WIDTH * 16, + SurfaceFormat::A8); + if (!glyphMask) { + return nullptr; + } + glyphDrawTarget->MaskSurface(ColorPattern(aColor), glyphMask, Point(0, 0), + DrawOptions(1.0f, CompositionOp::OP_SOURCE)); + RefPtr<SourceSurface> surface = glyphDrawTarget->Snapshot(); + if (!surface) { + return nullptr; + } + return new GlyphAtlas(std::move(surface), aColor); +} + +/** + * Reuse the current mini-font atlas if the color matches, otherwise regenerate + * it. + */ +static inline already_AddRefed<SourceSurface> GetGlyphAtlas( + const DeviceColor& aColor) { + // Get the opaque color, ignoring any transparency which will be handled + // later. + DeviceColor color(aColor.r, aColor.g, aColor.b); + + // Atomically grab the current GlyphAtlas pointer (if any). Because we + // exchange with nullptr here, no other thread will be able to touch the + // currAtlas record while we're using it; if they try, they'll just see + // the null that we stored. + GlyphAtlas* currAtlas = gGlyphAtlas.exchange(nullptr); + + if (currAtlas && currAtlas->Color() == color) { + // If its color is right, grab a reference to its surface. + RefPtr<SourceSurface> surface = currAtlas->Surface(); + // Now put the currAtlas record back in the global. If some other thread + // has stored an atlas there in the meantime, we just discard it. + delete gGlyphAtlas.exchange(currAtlas); + return surface.forget(); + } + + // Make a new atlas in the color we want. + GlyphAtlas* atlas = MakeGlyphAtlas(color); + RefPtr<SourceSurface> surface = atlas ? atlas->Surface() : nullptr; + + // Store the newly-created atlas in the global; release any other. + delete gGlyphAtlas.exchange(atlas); + return surface.forget(); +} + +/** + * Clear any cached glyph atlas resources. + */ +static void PurgeGlyphAtlas() { delete gGlyphAtlas.exchange(nullptr); } + +// WebRender layer manager user data that will get signaled when the layer +// manager is destroyed. +class WRUserData : public layers::LayerUserData, + public LinkedListElement<WRUserData> { + public: + explicit WRUserData(layers::WebRenderLayerManager* aManager); + + ~WRUserData(); + + static void Assign(layers::WebRenderLayerManager* aManager) { + if (!aManager->HasUserData(&sWRUserDataKey)) { + aManager->SetUserData(&sWRUserDataKey, new WRUserData(aManager)); + } + } + + void Remove() { mManager->RemoveUserData(&sWRUserDataKey); } + + layers::WebRenderLayerManager* mManager; + + static UserDataKey sWRUserDataKey; +}; + +static void DestroyImageKey(void* aClosure) { + auto* key = static_cast<wr::ImageKey*>(aClosure); + delete key; +} + +static RefPtr<SourceSurface> gWRGlyphAtlas[8]; +static LinkedList<WRUserData> gWRUsers; +UserDataKey WRUserData::sWRUserDataKey; + +/** + * Generates a transformed WebRender mini-font atlas for a given orientation. + */ +static already_AddRefed<SourceSurface> MakeWRGlyphAtlas(const Matrix* aMat) { + IntSize size(MINIFONT_WIDTH * 16, MINIFONT_HEIGHT); + // If the orientation is transposed, width/height are swapped. + if (aMat && aMat->_11 == 0) { + std::swap(size.width, size.height); + } + RefPtr<DrawTarget> ref = + gfxPlatform::GetPlatform()->ScreenReferenceDrawTarget(); + RefPtr<DrawTarget> dt = + gfxPlatform::GetPlatform()->CreateSimilarSoftwareDrawTarget( + ref, size, SurfaceFormat::B8G8R8A8); + if (!dt) { + return nullptr; + } + if (aMat) { + // Select appropriate transform matrix based on whether the + // orientation is transposed. + dt->SetTransform(aMat->_11 == 0 + ? Matrix(0.0f, copysign(1.0f, aMat->_12), + copysign(1.0f, aMat->_21), 0.0f, + aMat->_21 < 0 ? MINIFONT_HEIGHT : 0.0f, + aMat->_12 < 0 ? MINIFONT_WIDTH * 16 : 0.0f) + : Matrix(copysign(1.0f, aMat->_11), 0.0f, 0.0f, + copysign(1.0f, aMat->_22), + aMat->_11 < 0 ? MINIFONT_WIDTH * 16 : 0.0f, + aMat->_22 < 0 ? MINIFONT_HEIGHT : 0.0f)); + } + RefPtr<SourceSurface> mask = dt->CreateSourceSurfaceFromData( + const_cast<uint8_t*>(gMiniFontData), + IntSize(MINIFONT_WIDTH * 16, MINIFONT_HEIGHT), MINIFONT_WIDTH * 16, + SurfaceFormat::A8); + if (!mask) { + return nullptr; + } + dt->MaskSurface(ColorPattern(DeviceColor::MaskOpaqueWhite()), mask, + Point(0, 0)); + return dt->Snapshot(); +} + +/** + * Clear any cached WebRender glyph atlas resources. + */ +static void PurgeWRGlyphAtlas() { + // For each WR layer manager, we need go through each atlas orientation + // and see if it has a stashed image key. If it does, remove the image + // from the layer manager. + for (WRUserData* user : gWRUsers) { + auto* manager = user->mManager; + for (size_t i = 0; i < 8; i++) { + if (gWRGlyphAtlas[i]) { + auto* key = static_cast<wr::ImageKey*>(gWRGlyphAtlas[i]->GetUserData( + reinterpret_cast<UserDataKey*>(manager))); + if (key) { + manager->GetRenderRootStateManager()->AddImageKeyForDiscard(*key); + } + } + } + } + // Remove the layer managers' destroy notifications only after processing + // so as not to mess up gWRUsers iteration. + while (!gWRUsers.isEmpty()) { + gWRUsers.popFirst()->Remove(); + } + // Finally, clear out the atlases. + for (size_t i = 0; i < 8; i++) { + gWRGlyphAtlas[i] = nullptr; + } +} + +WRUserData::WRUserData(layers::WebRenderLayerManager* aManager) + : mManager(aManager) { + gWRUsers.insertFront(this); +} + +WRUserData::~WRUserData() { + // When the layer manager is destroyed, we need go through each + // atlas and remove any assigned image keys. + if (isInList()) { + for (size_t i = 0; i < 8; i++) { + if (gWRGlyphAtlas[i]) { + gWRGlyphAtlas[i]->RemoveUserData( + reinterpret_cast<UserDataKey*>(mManager)); + } + } + } +} + +static already_AddRefed<SourceSurface> GetWRGlyphAtlas(DrawTarget& aDrawTarget, + const Matrix* aMat) { + uint32_t key = 0; + // Encode orientation in the key. + if (aMat) { + if (aMat->_11 == 0) { + key |= 4 | (aMat->_12 < 0 ? 1 : 0) | (aMat->_21 < 0 ? 2 : 0); + } else { + key |= (aMat->_11 < 0 ? 1 : 0) | (aMat->_22 < 0 ? 2 : 0); + } + } + + // Check if an atlas was already created, or create one if necessary. + RefPtr<SourceSurface> atlas = gWRGlyphAtlas[key]; + if (!atlas) { + atlas = MakeWRGlyphAtlas(aMat); + gWRGlyphAtlas[key] = atlas; + } + + // The atlas may exist, but an image key may not be assigned for it to + // the given layer manager, or it may no longer be valid. + auto* tdt = static_cast<layout::TextDrawTarget*>(&aDrawTarget); + auto* manager = tdt->WrLayerManager(); + auto* imageKey = static_cast<wr::ImageKey*>( + atlas->GetUserData(reinterpret_cast<UserDataKey*>(manager))); + if (!imageKey || !manager->WrBridge()->MatchesNamespace(*imageKey)) { + // No image key, so we need to map the atlas' data for transfer to WR. + RefPtr<DataSourceSurface> dataSurface = atlas->GetDataSurface(); + if (!dataSurface) { + return nullptr; + } + DataSourceSurface::ScopedMap map(dataSurface, DataSourceSurface::READ); + if (!map.IsMapped()) { + return nullptr; + } + // Transfer the data and get an image key for it. + Maybe<wr::ImageKey> result = tdt->DefineImage( + atlas->GetSize(), map.GetStride(), atlas->GetFormat(), map.GetData()); + if (!result.isSome()) { + return nullptr; + } + // Assign the image key to the atlas. + atlas->AddUserData(reinterpret_cast<UserDataKey*>(manager), + new wr::ImageKey(result.ref()), DestroyImageKey); + // Create a user data notification for when the layer manager is + // destroyed so we can clean up any assigned image keys. + WRUserData::Assign(manager); + } + return atlas.forget(); +} + +static void DrawHexChar(uint32_t aDigit, Float aLeft, Float aTop, + DrawTarget& aDrawTarget, SourceSurface* aAtlas, + const DeviceColor& aColor, + const Matrix* aMat = nullptr) { + Rect dest(aLeft, aTop, MINIFONT_WIDTH, MINIFONT_HEIGHT); + if (aDrawTarget.GetBackendType() == BackendType::WEBRENDER_TEXT) { + // For WR, we need to get the image key assigned to the given WR layer + // manager for referencing the image. + auto* tdt = static_cast<layout::TextDrawTarget*>(&aDrawTarget); + auto* manager = tdt->WrLayerManager(); + auto* key = static_cast<wr::ImageKey*>( + aAtlas->GetUserData(reinterpret_cast<UserDataKey*>(manager))); + MOZ_ASSERT(key); + // Transform the bounds of the atlas into the given orientation, and then + // also transform a small clip rect which will be used to select the given + // digit from the atlas. + Rect bounds(aLeft - aDigit * MINIFONT_WIDTH, aTop, MINIFONT_WIDTH * 16, + MINIFONT_HEIGHT); + if (aMat) { + // Width and height may be negative after the transform, so move the rect + // if necessary and fix size. + bounds = aMat->TransformRect(bounds); + bounds.x += std::min(bounds.width, 0.0f); + bounds.y += std::min(bounds.height, 0.0f); + bounds.width = fabs(bounds.width); + bounds.height = fabs(bounds.height); + dest = aMat->TransformRect(dest); + dest.x += std::min(dest.width, 0.0f); + dest.y += std::min(dest.height, 0.0f); + dest.width = fabs(dest.width); + dest.height = fabs(dest.height); + } + // Finally, push the colored image with point filtering. + tdt->PushImage(*key, bounds, dest, wr::ImageRendering::Pixelated, + wr::ToColorF(aColor)); + } else { + // For the normal case, just draw the given digit from the atlas. Point + // filtering is used to ensure the mini-font rectangles stay sharp with any + // scaling. Handle any transparency here as well. + aDrawTarget.DrawSurface( + aAtlas, dest, + Rect(aDigit * MINIFONT_WIDTH, 0, MINIFONT_WIDTH, MINIFONT_HEIGHT), + DrawSurfaceOptions(SamplingFilter::POINT), + DrawOptions(aColor.a, CompositionOp::OP_OVER, AntialiasMode::NONE)); + } +} + +void gfxFontMissingGlyphs::Purge() { + PurgeGlyphAtlas(); + PurgeWRGlyphAtlas(); +} + +#else // MOZ_GFX_OPTIMIZE_MOBILE + +void gfxFontMissingGlyphs::Purge() {} + +#endif + +void gfxFontMissingGlyphs::Shutdown() { Purge(); } + +void gfxFontMissingGlyphs::DrawMissingGlyph(uint32_t aChar, const Rect& aRect, + DrawTarget& aDrawTarget, + const Pattern& aPattern, + const Matrix* aMat) { + Rect rect(aRect); + // If there is an orientation transform, reorient the bounding rect. + if (aMat) { + rect.MoveBy(-aRect.BottomLeft()); + rect = aMat->TransformBounds(rect); + rect.MoveBy(aRect.BottomLeft()); + } + + // If we're currently drawing with some kind of pattern, we just draw the + // missing-glyph data in black. + DeviceColor color = aPattern.GetType() == PatternType::COLOR + ? static_cast<const ColorPattern&>(aPattern).mColor + : ToDeviceColor(sRGBColor::OpaqueBlack()); + + // Stroke a rectangle so that the stroke's left edge is inset one pixel + // from the left edge of the glyph box and the stroke's right edge + // is inset one pixel from the right edge of the glyph box. + Float halfBorderWidth = BOX_BORDER_WIDTH / 2.0; + Float borderLeft = rect.X() + BOX_HORIZONTAL_INSET + halfBorderWidth; + Float borderRight = rect.XMost() - BOX_HORIZONTAL_INSET - halfBorderWidth; + Rect borderStrokeRect(borderLeft, rect.Y() + halfBorderWidth, + borderRight - borderLeft, + rect.Height() - 2.0 * halfBorderWidth); + if (!borderStrokeRect.IsEmpty()) { + ColorPattern adjustedColor(color); + adjustedColor.mColor.a *= BOX_BORDER_OPACITY; +#ifdef MOZ_GFX_OPTIMIZE_MOBILE + aDrawTarget.FillRect(borderStrokeRect, adjustedColor); +#else + StrokeOptions strokeOptions(BOX_BORDER_WIDTH); + aDrawTarget.StrokeRect(borderStrokeRect, adjustedColor, strokeOptions); +#endif + } + +#ifndef MOZ_GFX_OPTIMIZE_MOBILE + RefPtr<SourceSurface> atlas = + aDrawTarget.GetBackendType() == BackendType::WEBRENDER_TEXT + ? GetWRGlyphAtlas(aDrawTarget, aMat) + : GetGlyphAtlas(color); + if (!atlas) { + return; + } + + Point center = rect.Center(); + Float halfGap = HEX_CHAR_GAP / 2.f; + Float top = -(MINIFONT_HEIGHT + halfGap); + + // Figure out a scaling factor that will fit the glyphs in the target rect + // both horizontally and vertically. + Float width = HEX_CHAR_GAP + MINIFONT_WIDTH + HEX_CHAR_GAP + MINIFONT_WIDTH + + ((aChar < 0x10000) ? 0 : HEX_CHAR_GAP + MINIFONT_WIDTH) + + HEX_CHAR_GAP; + Float height = HEX_CHAR_GAP + MINIFONT_HEIGHT + HEX_CHAR_GAP + + MINIFONT_HEIGHT + HEX_CHAR_GAP; + Float scaling = std::min(rect.Height() / height, rect.Width() / width); + + // We always want integer scaling, otherwise the "bitmap" glyphs will look + // even uglier than usual when scaled to the target. + int32_t devPixelsPerCSSPx = std::max<int32_t>(1, std::floor(scaling)); + + Matrix tempMat; + if (aMat) { + // If there is an orientation transform, since draw target transforms may + // not be supported, scale and translate it so that it can be directly used + // for rendering the mini font without changing the draw target transform. + tempMat = Matrix(*aMat) + .PostScale(devPixelsPerCSSPx, devPixelsPerCSSPx) + .PostTranslate(center); + aMat = &tempMat; + } else { + // Otherwise, scale and translate the draw target transform assuming it + // supports that. + tempMat = aDrawTarget.GetTransform(); + aDrawTarget.SetTransform(Matrix(tempMat).PreTranslate(center).PreScale( + devPixelsPerCSSPx, devPixelsPerCSSPx)); + } + + if (aChar < 0x10000) { + if (rect.Width() >= 2 * (MINIFONT_WIDTH + HEX_CHAR_GAP) && + rect.Height() >= 2 * MINIFONT_HEIGHT + HEX_CHAR_GAP) { + // Draw 4 digits for BMP + Float left = -(MINIFONT_WIDTH + halfGap); + DrawHexChar((aChar >> 12) & 0xF, left, top, aDrawTarget, atlas, color, + aMat); + DrawHexChar((aChar >> 8) & 0xF, halfGap, top, aDrawTarget, atlas, color, + aMat); + DrawHexChar((aChar >> 4) & 0xF, left, halfGap, aDrawTarget, atlas, color, + aMat); + DrawHexChar(aChar & 0xF, halfGap, halfGap, aDrawTarget, atlas, color, + aMat); + } + } else { + if (rect.Width() >= 3 * (MINIFONT_WIDTH + HEX_CHAR_GAP) && + rect.Height() >= 2 * MINIFONT_HEIGHT + HEX_CHAR_GAP) { + // Draw 6 digits for non-BMP + Float first = -(MINIFONT_WIDTH * 1.5 + HEX_CHAR_GAP); + Float second = -(MINIFONT_WIDTH / 2.0); + Float third = (MINIFONT_WIDTH / 2.0 + HEX_CHAR_GAP); + DrawHexChar((aChar >> 20) & 0xF, first, top, aDrawTarget, atlas, color, + aMat); + DrawHexChar((aChar >> 16) & 0xF, second, top, aDrawTarget, atlas, color, + aMat); + DrawHexChar((aChar >> 12) & 0xF, third, top, aDrawTarget, atlas, color, + aMat); + DrawHexChar((aChar >> 8) & 0xF, first, halfGap, aDrawTarget, atlas, color, + aMat); + DrawHexChar((aChar >> 4) & 0xF, second, halfGap, aDrawTarget, atlas, + color, aMat); + DrawHexChar(aChar & 0xF, third, halfGap, aDrawTarget, atlas, color, aMat); + } + } + + if (!aMat) { + // The draw target transform was changed, so it must be restored to + // the original value. + aDrawTarget.SetTransform(tempMat); + } +#endif +} + +Float gfxFontMissingGlyphs::GetDesiredMinWidth(uint32_t aChar, + uint32_t aAppUnitsPerDevPixel) { + /** + * The minimum desired width for a missing-glyph glyph box. I've laid it out + * like this so you can see what goes where. + */ + Float width = BOX_HORIZONTAL_INSET + BOX_BORDER_WIDTH + HEX_CHAR_GAP + + MINIFONT_WIDTH + HEX_CHAR_GAP + MINIFONT_WIDTH + + ((aChar < 0x10000) ? 0 : HEX_CHAR_GAP + MINIFONT_WIDTH) + + HEX_CHAR_GAP + BOX_BORDER_WIDTH + BOX_HORIZONTAL_INSET; + // Note that this will give us floating-point division, so the width will + // -not- be snapped to integer multiples of its basic pixel value + width *= Float(AppUnitsPerCSSPixel()) / aAppUnitsPerDevPixel; + return width; +} |