/* 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 "gfxSVGGlyphs.h" #include "mozilla/BasePrincipal.h" #include "mozilla/LoadInfo.h" #include "mozilla/NullPrincipal.h" #include "mozilla/PresShell.h" #include "mozilla/SMILAnimationController.h" #include "mozilla/SVGContextPaint.h" #include "mozilla/SVGUtils.h" #include "mozilla/dom/Document.h" #include "mozilla/dom/Element.h" #include "mozilla/dom/FontTableURIProtocolHandler.h" #include "mozilla/dom/ImageTracker.h" #include "mozilla/dom/SVGDocument.h" #include "nsError.h" #include "nsString.h" #include "nsICategoryManager.h" #include "nsIDocumentLoaderFactory.h" #include "nsIContentViewer.h" #include "nsIStreamListener.h" #include "nsServiceManagerUtils.h" #include "nsNetUtil.h" #include "nsIInputStream.h" #include "nsStringStream.h" #include "nsStreamUtils.h" #include "nsIPrincipal.h" #include "nsContentUtils.h" #include "gfxFont.h" #include "gfxContext.h" #include "harfbuzz/hb.h" #include "zlib.h" #define SVG_CONTENT_TYPE "image/svg+xml"_ns #define UTF8_CHARSET "utf-8"_ns using namespace mozilla; using mozilla::dom::Document; using mozilla::dom::Element; /* static */ const mozilla::gfx::DeviceColor SimpleTextContextPaint::sZero; gfxSVGGlyphs::gfxSVGGlyphs(hb_blob_t* aSVGTable, gfxFontEntry* aFontEntry) : mSVGData(aSVGTable), mFontEntry(aFontEntry) { unsigned int length; const char* svgData = hb_blob_get_data(mSVGData, &length); mHeader = reinterpret_cast(svgData); mDocIndex = nullptr; if (sizeof(Header) <= length && uint16_t(mHeader->mVersion) == 0 && uint64_t(mHeader->mDocIndexOffset) + 2 <= length) { const DocIndex* docIndex = reinterpret_cast(svgData + mHeader->mDocIndexOffset); // Limit the number of documents to avoid overflow if (uint64_t(mHeader->mDocIndexOffset) + 2 + uint16_t(docIndex->mNumEntries) * sizeof(IndexEntry) <= length) { mDocIndex = docIndex; } } } gfxSVGGlyphs::~gfxSVGGlyphs() { hb_blob_destroy(mSVGData); } void gfxSVGGlyphs::DidRefresh() { mFontEntry->NotifyGlyphsChanged(); } /* * Comparison operator for finding a range containing a given glyph ID. Simply * checks whether |key| is less (greater) than every element of |range|, in * which case return |key| < |range| (|key| > |range|). Otherwise |key| is in * |range|, in which case return equality. * The total ordering here is guaranteed by * (1) the index ranges being disjoint; and * (2) the (sole) key always being a singleton, so intersection => containment * (note that this is wrong if we have more than one intersection or two * sets intersecting of size > 1 -- so... don't do that) */ /* static */ int gfxSVGGlyphs::CompareIndexEntries(const void* aKey, const void* aEntry) { const uint32_t key = *(uint32_t*)aKey; const IndexEntry* entry = (const IndexEntry*)aEntry; if (key < uint16_t(entry->mStartGlyph)) { return -1; } if (key > uint16_t(entry->mEndGlyph)) { return 1; } return 0; } gfxSVGGlyphsDocument* gfxSVGGlyphs::FindOrCreateGlyphsDocument( uint32_t aGlyphId) { if (!mDocIndex) { // Invalid table return nullptr; } IndexEntry* entry = (IndexEntry*)bsearch( &aGlyphId, mDocIndex->mEntries, uint16_t(mDocIndex->mNumEntries), sizeof(IndexEntry), CompareIndexEntries); if (!entry) { return nullptr; } return mGlyphDocs.WithEntryHandle( entry->mDocOffset, [&](auto&& glyphDocsEntry) -> gfxSVGGlyphsDocument* { if (!glyphDocsEntry) { unsigned int length; const uint8_t* data = (const uint8_t*)hb_blob_get_data(mSVGData, &length); if (entry->mDocOffset > 0 && uint64_t(mHeader->mDocIndexOffset) + entry->mDocOffset + entry->mDocLength <= length) { return glyphDocsEntry .Insert(MakeUnique( data + mHeader->mDocIndexOffset + entry->mDocOffset, entry->mDocLength, this)) .get(); } return nullptr; } return glyphDocsEntry->get(); }); } nsresult gfxSVGGlyphsDocument::SetupPresentation() { nsCOMPtr catMan = do_GetService(NS_CATEGORYMANAGER_CONTRACTID); nsCString contractId; nsresult rv = catMan->GetCategoryEntry("Gecko-Content-Viewers", "image/svg+xml", contractId); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr docLoaderFactory = do_GetService(contractId.get()); NS_ASSERTION(docLoaderFactory, "Couldn't get DocumentLoaderFactory"); nsCOMPtr viewer; rv = docLoaderFactory->CreateInstanceForDocument(nullptr, mDocument, nullptr, getter_AddRefs(viewer)); NS_ENSURE_SUCCESS(rv, rv); auto upem = mOwner->FontEntry()->UnitsPerEm(); rv = viewer->Init(nullptr, gfx::IntRect(0, 0, upem, upem), nullptr); if (NS_SUCCEEDED(rv)) { rv = viewer->Open(nullptr, nullptr); NS_ENSURE_SUCCESS(rv, rv); } RefPtr presShell = viewer->GetPresShell(); if (!presShell->DidInitialize()) { rv = presShell->Initialize(); NS_ENSURE_SUCCESS(rv, rv); } mDocument->FlushPendingNotifications(FlushType::Layout); if (mDocument->HasAnimationController()) { mDocument->GetAnimationController()->Resume(SMILTimeContainer::PAUSE_IMAGE); } mDocument->ImageTracker()->SetAnimatingState(true); mViewer = viewer; mPresShell = presShell; mPresShell->AddPostRefreshObserver(this); return NS_OK; } void gfxSVGGlyphsDocument::DidRefresh() { mOwner->DidRefresh(); } /** * Walk the DOM tree to find all glyph elements and insert them into the lookup * table * @param aElem The element to search from */ void gfxSVGGlyphsDocument::FindGlyphElements(Element* aElem) { for (nsIContent* child = aElem->GetLastChild(); child; child = child->GetPreviousSibling()) { if (!child->IsElement()) { continue; } FindGlyphElements(child->AsElement()); } InsertGlyphId(aElem); } /** * If there exists an SVG glyph with the specified glyph id, render it and * return true If no such glyph exists, or in the case of an error return false * @param aContext The thebes aContext to draw to * @param aGlyphId The glyph id * @return true iff rendering succeeded */ void gfxSVGGlyphs::RenderGlyph(gfxContext* aContext, uint32_t aGlyphId, SVGContextPaint* aContextPaint) { gfxContextAutoSaveRestore aContextRestorer(aContext); Element* glyph = mGlyphIdMap.Get(aGlyphId); MOZ_ASSERT(glyph, "No glyph element. Should check with HasSVGGlyph() first!"); AutoSetRestoreSVGContextPaint autoSetRestore( *aContextPaint, *glyph->OwnerDoc()->AsSVGDocument()); SVGUtils::PaintSVGGlyph(glyph, aContext); #if DEBUG // This will not have any effect, because we're about to restore the state // via the aContextRestorer destructor, but it prevents debug builds from // asserting if it turns out that PaintSVGGlyph didn't actually do anything. // This happens if the SVG document consists of just an image, and the image // hasn't finished loading yet so we can't draw it. aContext->SetOp(gfx::CompositionOp::OP_OVER); #endif } bool gfxSVGGlyphs::GetGlyphExtents(uint32_t aGlyphId, const gfxMatrix& aSVGToAppSpace, gfxRect* aResult) { Element* glyph = mGlyphIdMap.Get(aGlyphId); NS_ASSERTION(glyph, "No glyph element. Should check with HasSVGGlyph() first!"); return SVGUtils::GetSVGGlyphExtents(glyph, aSVGToAppSpace, aResult); } Element* gfxSVGGlyphs::GetGlyphElement(uint32_t aGlyphId) { return mGlyphIdMap.LookupOrInsertWith(aGlyphId, [&] { Element* elem = nullptr; if (gfxSVGGlyphsDocument* set = FindOrCreateGlyphsDocument(aGlyphId)) { elem = set->GetGlyphElement(aGlyphId); } return elem; }); } bool gfxSVGGlyphs::HasSVGGlyph(uint32_t aGlyphId) { return !!GetGlyphElement(aGlyphId); } size_t gfxSVGGlyphs::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const { // We don't include the size of mSVGData here, because (depending on the // font backend implementation) it will either wrap a block of data owned // by the system (and potentially shared), or a table that's in our font // table cache and therefore already counted. size_t result = aMallocSizeOf(this) + mGlyphDocs.ShallowSizeOfExcludingThis(aMallocSizeOf) + mGlyphIdMap.ShallowSizeOfExcludingThis(aMallocSizeOf); for (const auto& entry : mGlyphDocs.Values()) { result += entry->SizeOfIncludingThis(aMallocSizeOf); } return result; } Element* gfxSVGGlyphsDocument::GetGlyphElement(uint32_t aGlyphId) { return mGlyphIdMap.Get(aGlyphId); } gfxSVGGlyphsDocument::gfxSVGGlyphsDocument(const uint8_t* aBuffer, uint32_t aBufLen, gfxSVGGlyphs* aSVGGlyphs) : mOwner(aSVGGlyphs) { if (aBufLen >= 14 && aBuffer[0] == 31 && aBuffer[1] == 139) { // It's a gzip-compressed document; decompress it before parsing. // The original length (modulo 2^32) is found in the last 4 bytes // of the data, stored in little-endian format. We read it as // individual bytes to avoid possible alignment issues. // (Note that if the original length was >2^32, then origLen here // will be incorrect; but then the inflate() call will not return // Z_STREAM_END and we'll bail out safely.) size_t origLen = (size_t(aBuffer[aBufLen - 1]) << 24) + (size_t(aBuffer[aBufLen - 2]) << 16) + (size_t(aBuffer[aBufLen - 3]) << 8) + size_t(aBuffer[aBufLen - 4]); AutoTArray outBuf; if (outBuf.SetLength(origLen, mozilla::fallible)) { z_stream s = {0}; s.next_in = const_cast(aBuffer); s.avail_in = aBufLen; s.next_out = outBuf.Elements(); s.avail_out = outBuf.Length(); // The magic number 16 here is the zlib flag to expect gzip format, // see http://www.zlib.net/manual.html#Advanced if (Z_OK == inflateInit2(&s, 16 + MAX_WBITS)) { int result = inflate(&s, Z_FINISH); if (Z_STREAM_END == result) { MOZ_ASSERT(size_t(s.next_out - outBuf.Elements()) == origLen); ParseDocument(outBuf.Elements(), outBuf.Length()); } else { NS_WARNING("Failed to decompress SVG glyphs document"); } inflateEnd(&s); } } else { NS_WARNING("Failed to allocate memory for SVG glyphs document"); } } else { ParseDocument(aBuffer, aBufLen); } if (!mDocument) { NS_WARNING("Could not parse SVG glyphs document"); return; } Element* root = mDocument->GetRootElement(); if (!root) { NS_WARNING("Could not parse SVG glyphs document"); return; } nsresult rv = SetupPresentation(); if (NS_FAILED(rv)) { NS_WARNING("Couldn't setup presentation for SVG glyphs document"); return; } FindGlyphElements(root); } gfxSVGGlyphsDocument::~gfxSVGGlyphsDocument() { if (mDocument) { mDocument->OnPageHide(false, nullptr); } if (mPresShell) { mPresShell->RemovePostRefreshObserver(this); } if (mViewer) { mViewer->Close(nullptr); mViewer->Destroy(); } } static nsresult CreateBufferedStream(const uint8_t* aBuffer, uint32_t aBufLen, nsCOMPtr& aResult) { nsCOMPtr stream; nsresult rv = NS_NewByteInputStream( getter_AddRefs(stream), Span(reinterpret_cast(aBuffer), aBufLen), NS_ASSIGNMENT_DEPEND); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr aBufferedStream; if (!NS_InputStreamIsBuffered(stream)) { rv = NS_NewBufferedInputStream(getter_AddRefs(aBufferedStream), stream.forget(), 4096); NS_ENSURE_SUCCESS(rv, rv); stream = aBufferedStream; } aResult = stream; return NS_OK; } nsresult gfxSVGGlyphsDocument::ParseDocument(const uint8_t* aBuffer, uint32_t aBufLen) { // Mostly pulled from nsDOMParser::ParseFromStream nsCOMPtr stream; nsresult rv = CreateBufferedStream(aBuffer, aBufLen, stream); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr uri; mozilla::dom::FontTableURIProtocolHandler::GenerateURIString( mSVGGlyphsDocumentURI); rv = NS_NewURI(getter_AddRefs(uri), mSVGGlyphsDocumentURI); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr principal = NullPrincipal::CreateWithoutOriginAttributes(); RefPtr document; rv = NS_NewDOMDocument(getter_AddRefs(document), u""_ns, // aNamespaceURI u""_ns, // aQualifiedName nullptr, // aDoctype uri, uri, principal, false, // aLoadedAsData nullptr, // aEventObject DocumentFlavorSVG); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr channel; rv = NS_NewInputStreamChannel( getter_AddRefs(channel), uri, nullptr, // aStream principal, nsILoadInfo::SEC_FORCE_INHERIT_PRINCIPAL, nsIContentPolicy::TYPE_OTHER, SVG_CONTENT_TYPE, UTF8_CHARSET); NS_ENSURE_SUCCESS(rv, rv); // Set this early because various decisions during page-load depend on it. document->SetIsBeingUsedAsImage(); document->SetIsSVGGlyphsDocument(); document->SetReadyStateInternal(Document::READYSTATE_UNINITIALIZED); nsCOMPtr listener; rv = document->StartDocumentLoad("external-resource", channel, nullptr, // aLoadGroup nullptr, // aContainer getter_AddRefs(listener), true /* aReset */); if (NS_FAILED(rv) || !listener) { return NS_ERROR_FAILURE; } rv = listener->OnStartRequest(channel); if (NS_FAILED(rv)) { channel->Cancel(rv); } nsresult status; channel->GetStatus(&status); if (NS_SUCCEEDED(rv) && NS_SUCCEEDED(status)) { rv = listener->OnDataAvailable(channel, stream, 0, aBufLen); if (NS_FAILED(rv)) { channel->Cancel(rv); } channel->GetStatus(&status); } rv = listener->OnStopRequest(channel, status); NS_ENSURE_SUCCESS(rv, NS_ERROR_FAILURE); document.swap(mDocument); return NS_OK; } void gfxSVGGlyphsDocument::InsertGlyphId(Element* aGlyphElement) { nsAutoString glyphIdStr; static const uint32_t glyphPrefixLength = 5; // The maximum glyph ID is 65535 so the maximum length of the numeric part // is 5. if (!aGlyphElement->GetAttr(kNameSpaceID_None, nsGkAtoms::id, glyphIdStr) || !StringBeginsWith(glyphIdStr, u"glyph"_ns) || glyphIdStr.Length() > glyphPrefixLength + 5) { return; } uint32_t id = 0; for (uint32_t i = glyphPrefixLength; i < glyphIdStr.Length(); ++i) { char16_t ch = glyphIdStr.CharAt(i); if (ch < '0' || ch > '9') { return; } if (ch == '0' && i == glyphPrefixLength) { return; } id = id * 10 + (ch - '0'); } mGlyphIdMap.InsertOrUpdate(id, aGlyphElement); } size_t gfxSVGGlyphsDocument::SizeOfIncludingThis( mozilla::MallocSizeOf aMallocSizeOf) const { return aMallocSizeOf(this) + mGlyphIdMap.ShallowSizeOfExcludingThis(aMallocSizeOf) + mSVGGlyphsDocumentURI.SizeOfExcludingThisIfUnshared(aMallocSizeOf); }