/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ /* * This file is part of the LibreOffice project. * * 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace sw { namespace { std::shared_ptr lclAddIssue(sfx::AccessibilityIssueCollection& rIssueCollection, OUString const& rText, sfx::AccessibilityIssueID eIssue = sfx::AccessibilityIssueID::UNSPECIFIED) { auto pIssue = std::make_shared(eIssue); pIssue->m_aIssueText = rText; rIssueCollection.getIssues().push_back(pIssue); return pIssue; } class BaseCheck { protected: sfx::AccessibilityIssueCollection& m_rIssueCollection; public: BaseCheck(sfx::AccessibilityIssueCollection& rIssueCollection) : m_rIssueCollection(rIssueCollection) { } virtual ~BaseCheck() {} }; class NodeCheck : public BaseCheck { public: NodeCheck(sfx::AccessibilityIssueCollection& rIssueCollection) : BaseCheck(rIssueCollection) { } virtual void check(SwNode* pCurrent) = 0; }; // Check NoTextNodes: Graphic, OLE for alt (title) text class NoTextNodeAltTextCheck : public NodeCheck { void checkNoTextNode(SwNoTextNode* pNoTextNode) { if (!pNoTextNode) return; OUString sAlternative = pNoTextNode->GetTitle(); if (sAlternative.isEmpty()) { OUString sName = pNoTextNode->GetFlyFormat()->GetName(); OUString sIssueText = SwResId(STR_NO_ALT).replaceAll("%OBJECT_NAME%", sName); if (pNoTextNode->IsOLENode()) { auto pIssue = lclAddIssue(m_rIssueCollection, sIssueText, sfx::AccessibilityIssueID::NO_ALT_OLE); pIssue->setDoc(pNoTextNode->GetDoc()); pIssue->setIssueObject(IssueObject::OLE); pIssue->setObjectID(pNoTextNode->GetFlyFormat()->GetName()); } else if (pNoTextNode->IsGrfNode()) { auto pIssue = lclAddIssue(m_rIssueCollection, sIssueText, sfx::AccessibilityIssueID::NO_ALT_GRAPHIC); pIssue->setDoc(pNoTextNode->GetDoc()); pIssue->setIssueObject(IssueObject::GRAPHIC); pIssue->setObjectID(pNoTextNode->GetFlyFormat()->GetName()); } } } public: NoTextNodeAltTextCheck(sfx::AccessibilityIssueCollection& rIssueCollection) : NodeCheck(rIssueCollection) { } void check(SwNode* pCurrent) override { if (pCurrent->GetNodeType() & SwNodeType::NoTextMask) { SwNoTextNode* pNoTextNode = pCurrent->GetNoTextNode(); if (pNoTextNode) checkNoTextNode(pNoTextNode); } } }; // Check Table node if the table is merged and split. class TableNodeMergeSplitCheck : public NodeCheck { private: void addTableIssue(SwTable const& rTable, SwDoc* pDoc) { const SwTableFormat* pFormat = rTable.GetFrameFormat(); OUString sName = pFormat->GetName(); OUString sIssueText = SwResId(STR_TABLE_MERGE_SPLIT).replaceAll("%OBJECT_NAME%", sName); auto pIssue = lclAddIssue(m_rIssueCollection, sIssueText, sfx::AccessibilityIssueID::TABLE_MERGE_SPLIT); pIssue->setDoc(pDoc); pIssue->setIssueObject(IssueObject::TABLE); pIssue->setObjectID(sName); } void checkTableNode(SwTableNode* pTableNode) { if (!pTableNode) return; SwTable const& rTable = pTableNode->GetTable(); SwDoc* pDoc = pTableNode->GetDoc(); if (rTable.IsTableComplex()) { addTableIssue(rTable, pDoc); } else { if (rTable.GetTabLines().size() > 1) { int i = 0; size_t nFirstLineSize = 0; bool bAllColumnsSameSize = true; bool bCellSpansOverMoreRows = false; for (SwTableLine const* pTableLine : rTable.GetTabLines()) { if (i == 0) { nFirstLineSize = pTableLine->GetTabBoxes().size(); } else { size_t nLineSize = pTableLine->GetTabBoxes().size(); if (nFirstLineSize != nLineSize) { bAllColumnsSameSize = false; } } i++; // Check for row span in each table box (cell) for (SwTableBox const* pBox : pTableLine->GetTabBoxes()) { if (pBox->getRowSpan() > 1) bCellSpansOverMoreRows = true; } } if (!bAllColumnsSameSize || bCellSpansOverMoreRows) { addTableIssue(rTable, pDoc); } } } } public: TableNodeMergeSplitCheck(sfx::AccessibilityIssueCollection& rIssueCollection) : NodeCheck(rIssueCollection) { } void check(SwNode* pCurrent) override { if (pCurrent->GetNodeType() & SwNodeType::Table) { SwTableNode* pTableNode = pCurrent->GetTableNode(); if (pTableNode) checkTableNode(pTableNode); } } }; class NumberingCheck : public NodeCheck { private: SwTextNode* m_pPreviousTextNode; const std::vector> m_aNumberingCombinations{ { "1.", "2." }, { "(1)", "(2)" }, { "1)", "2)" }, { "a.", "b." }, { "(a)", "(b)" }, { "a)", "b)" }, { "A.", "B." }, { "(A)", "(B)" }, { "A)", "B)" } }; public: NumberingCheck(sfx::AccessibilityIssueCollection& rIssueCollection) : NodeCheck(rIssueCollection) , m_pPreviousTextNode(nullptr) { } void check(SwNode* pCurrent) override { if (pCurrent->IsTextNode()) { if (m_pPreviousTextNode) { for (auto& rPair : m_aNumberingCombinations) { if (pCurrent->GetTextNode()->GetText().startsWith(rPair.second) && m_pPreviousTextNode->GetText().startsWith(rPair.first)) { OUString sNumbering = rPair.first + " " + rPair.second + "..."; OUString sIssueText = SwResId(STR_FAKE_NUMBERING).replaceAll("%NUMBERING%", sNumbering); lclAddIssue(m_rIssueCollection, sIssueText); } } } m_pPreviousTextNode = pCurrent->GetTextNode(); } } }; class HyperlinkCheck : public NodeCheck { private: void checkTextRange(uno::Reference const& xTextRange) { uno::Reference xProperties(xTextRange, uno::UNO_QUERY); if (xProperties->getPropertySetInfo()->hasPropertyByName("HyperLinkURL")) { OUString sHyperlink; xProperties->getPropertyValue("HyperLinkURL") >>= sHyperlink; if (!sHyperlink.isEmpty()) { OUString sText = xTextRange->getString(); if (INetURLObject(sText) == INetURLObject(sHyperlink)) { OUString sIssueText = SwResId(STR_HYPERLINK_TEXT_IS_LINK).replaceFirst("%LINK%", sHyperlink); lclAddIssue(m_rIssueCollection, sIssueText); } } } } public: HyperlinkCheck(sfx::AccessibilityIssueCollection& rIssueCollection) : NodeCheck(rIssueCollection) { } void check(SwNode* pCurrent) override { if (pCurrent->IsTextNode()) { SwTextNode* pTextNode = pCurrent->GetTextNode(); uno::Reference xParagraph = SwXParagraph::CreateXParagraph(*pTextNode->GetDoc(), pTextNode); if (xParagraph.is()) { uno::Reference xRunEnumAccess(xParagraph, uno::UNO_QUERY); uno::Reference xRunEnum = xRunEnumAccess->createEnumeration(); while (xRunEnum->hasMoreElements()) { uno::Reference xRun(xRunEnum->nextElement(), uno::UNO_QUERY); if (xRun.is()) { checkTextRange(xRun); } } } } } }; // Based on https://www.w3.org/TR/WCAG21/#dfn-relative-luminance double calculateRelativeLuminance(Color const& rColor) { // Convert to BColor which has R, G, B colors components // represented by a floating point number from [0.0, 1.0] const basegfx::BColor aBColor = rColor.getBColor(); double r = aBColor.getRed(); double g = aBColor.getGreen(); double b = aBColor.getBlue(); // Calculate the values according to the described algorithm r = (r <= 0.03928) ? r / 12.92 : std::pow((r + 0.055) / 1.055, 2.4); g = (g <= 0.03928) ? g / 12.92 : std::pow((g + 0.055) / 1.055, 2.4); b = (b <= 0.03928) ? b / 12.92 : std::pow((b + 0.055) / 1.055, 2.4); return 0.2126 * r + 0.7152 * g + 0.0722 * b; } // TODO move to common color tools (BColorTools maybe) // Based on https://www.w3.org/TR/WCAG21/#dfn-contrast-ratio double calculateContrastRatio(Color const& rColor1, Color const& rColor2) { const double fLuminance1 = calculateRelativeLuminance(rColor1); const double fLuminance2 = calculateRelativeLuminance(rColor2); const std::pair aMinMax = std::minmax(fLuminance1, fLuminance2); // (L1 + 0.05) / (L2 + 0.05) // L1 is the lighter color (greater luminance value) // L2 is the darker color (smaller luminance value) return (aMinMax.second + 0.05) / (aMinMax.first + 0.05); } class TextContrastCheck : public NodeCheck { private: void checkTextRange(uno::Reference const& xTextRange, uno::Reference const& xParagraph, SwTextNode* pTextNode) { sal_Int32 nParaBackColor = {}; // spurious -Werror=maybe-uninitialized uno::Reference xParagraphProperties(xParagraph, uno::UNO_QUERY); if (!(xParagraphProperties->getPropertyValue("ParaBackColor") >>= nParaBackColor)) { SAL_WARN("sw.a11y", "ParaBackColor void"); return; } uno::Reference xProperties(xTextRange, uno::UNO_QUERY); if (xProperties.is()) { // Foreground color sal_Int32 nCharColor = {}; // spurious -Werror=maybe-uninitialized if (!(xProperties->getPropertyValue("CharColor") >>= nCharColor)) { // not sure this is impossible, can the default be void? SAL_WARN("sw.a11y", "CharColor void"); return; } Color aForegroundColor(nCharColor); if (aForegroundColor == COL_AUTO) return; const SwPageDesc* pPageDescription = pTextNode->FindPageDesc(); const SwFrameFormat& rPageFormat = pPageDescription->GetMaster(); const SwAttrSet& rPageSet = rPageFormat.GetAttrSet(); const XFillStyleItem* pXFillStyleItem( rPageSet.GetItem(XATTR_FILLSTYLE, false)); Color aPageBackground; if (pXFillStyleItem && pXFillStyleItem->GetValue() == css::drawing::FillStyle_SOLID) { const XFillColorItem* rXFillColorItem = rPageSet.GetItem(XATTR_FILLCOLOR, false); aPageBackground = rXFillColorItem->GetColorValue(); } sal_Int32 nCharBackColor = {}; // spurious -Werror=maybe-uninitialized if (!(xProperties->getPropertyValue("CharBackColor") >>= nCharBackColor)) { SAL_WARN("sw.a11y", "CharBackColor void"); return; } // Determine the background color // Try Character background (highlight) Color aBackgroundColor(nCharBackColor); // If not character background color, try paragraph background color if (aBackgroundColor == COL_AUTO) aBackgroundColor = Color(nParaBackColor); // If not paragraph background color, try page color if (aBackgroundColor == COL_AUTO) aBackgroundColor = aPageBackground; // If not page color, assume white background color if (aBackgroundColor == COL_AUTO) aBackgroundColor = COL_WHITE; double fContrastRatio = calculateContrastRatio(aForegroundColor, aBackgroundColor); if (fContrastRatio < 4.5) { lclAddIssue(m_rIssueCollection, SwResId(STR_TEXT_CONTRAST)); } } } public: TextContrastCheck(sfx::AccessibilityIssueCollection& rIssueCollection) : NodeCheck(rIssueCollection) { } void check(SwNode* pCurrent) override { if (pCurrent->IsTextNode()) { SwTextNode* pTextNode = pCurrent->GetTextNode(); uno::Reference xParagraph; xParagraph = SwXParagraph::CreateXParagraph(*pTextNode->GetDoc(), pTextNode); if (xParagraph.is()) { uno::Reference xRunEnumAccess(xParagraph, uno::UNO_QUERY); uno::Reference xRunEnum = xRunEnumAccess->createEnumeration(); while (xRunEnum->hasMoreElements()) { uno::Reference xRun(xRunEnum->nextElement(), uno::UNO_QUERY); if (xRun.is()) checkTextRange(xRun, xParagraph, pTextNode); } } } } }; class TextFormattingCheck : public NodeCheck { private: public: TextFormattingCheck(sfx::AccessibilityIssueCollection& rIssueCollection) : NodeCheck(rIssueCollection) { } void checkAutoFormat(SwTextNode* pTextNode, const SwTextAttr* pTextAttr) { const SwFormatAutoFormat& rAutoFormat = pTextAttr->GetAutoFormat(); SfxItemIter aItemIter(*rAutoFormat.GetStyleHandle()); const SfxPoolItem* pItem = aItemIter.GetCurItem(); std::vector aFormattings; while (pItem) { OUString sFormattingType; switch (pItem->Which()) { case RES_CHRATR_WEIGHT: case RES_CHRATR_CJK_WEIGHT: case RES_CHRATR_CTL_WEIGHT: sFormattingType = "Weight"; break; case RES_CHRATR_POSTURE: case RES_CHRATR_CJK_POSTURE: case RES_CHRATR_CTL_POSTURE: sFormattingType = "Posture"; break; case RES_CHRATR_SHADOWED: sFormattingType = "Shadowed"; break; case RES_CHRATR_COLOR: sFormattingType = "Font Color"; break; case RES_CHRATR_FONTSIZE: case RES_CHRATR_CJK_FONTSIZE: case RES_CHRATR_CTL_FONTSIZE: sFormattingType = "Font Size"; break; case RES_CHRATR_FONT: case RES_CHRATR_CJK_FONT: case RES_CHRATR_CTL_FONT: sFormattingType = "Font"; break; case RES_CHRATR_EMPHASIS_MARK: sFormattingType = "Emphasis Mark"; break; case RES_CHRATR_UNDERLINE: sFormattingType = "Underline"; break; case RES_CHRATR_OVERLINE: sFormattingType = "Overline"; break; case RES_CHRATR_CROSSEDOUT: sFormattingType = "Strikethrough"; break; case RES_CHRATR_RELIEF: sFormattingType = "Relief"; break; case RES_CHRATR_CONTOUR: sFormattingType = "Outline"; break; default: break; } if (!sFormattingType.isEmpty()) aFormattings.push_back(sFormattingType); pItem = aItemIter.NextItem(); } if (!aFormattings.empty()) { o3tl::remove_duplicates(aFormattings); auto pIssue = lclAddIssue(m_rIssueCollection, SwResId(STR_TEXT_FORMATTING_CONVEYS_MEANING), sfx::AccessibilityIssueID::TEXT_FORMATTING); pIssue->setIssueObject(IssueObject::TEXT); pIssue->setNode(pTextNode); SwDoc* pDocument = pTextNode->GetDoc(); pIssue->setDoc(pDocument); pIssue->setStart(pTextAttr->GetStart()); pIssue->setEnd(pTextAttr->GetAnyEnd()); } } void check(SwNode* pCurrent) override { if (pCurrent->IsTextNode()) { SwTextNode* pTextNode = pCurrent->GetTextNode(); if (pTextNode->HasHints()) { SwpHints& rHints = pTextNode->GetSwpHints(); for (size_t i = 0; i < rHints.Count(); ++i) { const SwTextAttr* pTextAttr = rHints.Get(i); if (pTextAttr->Which() == RES_TXTATR_AUTOFMT) { checkAutoFormat(pTextNode, pTextAttr); } } } } } }; class BlinkingTextCheck : public NodeCheck { private: void checkTextRange(uno::Reference const& xTextRange) { uno::Reference xProperties(xTextRange, uno::UNO_QUERY); if (xProperties.is() && xProperties->getPropertySetInfo()->hasPropertyByName("CharFlash")) { bool bBlinking = false; xProperties->getPropertyValue("CharFlash") >>= bBlinking; if (bBlinking) { lclAddIssue(m_rIssueCollection, SwResId(STR_TEXT_BLINKING)); } } } public: BlinkingTextCheck(sfx::AccessibilityIssueCollection& rIssueCollection) : NodeCheck(rIssueCollection) { } void check(SwNode* pCurrent) override { if (pCurrent->IsTextNode()) { SwTextNode* pTextNode = pCurrent->GetTextNode(); uno::Reference xParagraph; xParagraph = SwXParagraph::CreateXParagraph(*pTextNode->GetDoc(), pTextNode); if (xParagraph.is()) { uno::Reference xRunEnumAccess(xParagraph, uno::UNO_QUERY); uno::Reference xRunEnum = xRunEnumAccess->createEnumeration(); while (xRunEnum->hasMoreElements()) { uno::Reference xRun(xRunEnum->nextElement(), uno::UNO_QUERY); if (xRun.is()) checkTextRange(xRun); } } } } }; class HeaderCheck : public NodeCheck { private: int m_nPreviousLevel; public: HeaderCheck(sfx::AccessibilityIssueCollection& rIssueCollection) : NodeCheck(rIssueCollection) , m_nPreviousLevel(0) { } void check(SwNode* pCurrent) override { if (pCurrent->IsTextNode()) { SwTextNode* pTextNode = pCurrent->GetTextNode(); SwTextFormatColl* pCollection = pTextNode->GetTextColl(); int nLevel = pCollection->GetAssignedOutlineStyleLevel(); if (nLevel < 0) return; if (nLevel > m_nPreviousLevel && std::abs(nLevel - m_nPreviousLevel) > 1) { lclAddIssue(m_rIssueCollection, SwResId(STR_HEADINGS_NOT_IN_ORDER)); } m_nPreviousLevel = nLevel; } } }; class DocumentCheck : public BaseCheck { public: DocumentCheck(sfx::AccessibilityIssueCollection& rIssueCollection) : BaseCheck(rIssueCollection) { } virtual void check(SwDoc* pDoc) = 0; }; // Check default language class DocumentDefaultLanguageCheck : public DocumentCheck { public: DocumentDefaultLanguageCheck(sfx::AccessibilityIssueCollection& rIssueCollection) : DocumentCheck(rIssueCollection) { } void check(SwDoc* pDoc) override { // TODO maybe - also check RES_CHRATR_CJK_LANGUAGE, RES_CHRATR_CTL_LANGUAGE if CJK or CTL are enabled const SvxLanguageItem& rLang = pDoc->GetDefault(RES_CHRATR_LANGUAGE); LanguageType eLanguage = rLang.GetLanguage(); if (eLanguage == LANGUAGE_NONE) { lclAddIssue(m_rIssueCollection, SwResId(STR_DOCUMENT_DEFAULT_LANGUAGE), sfx::AccessibilityIssueID::DOCUMENT_LANGUAGE); } else { for (SwTextFormatColl* pTextFormatCollection : *pDoc->GetTextFormatColls()) { const SwAttrSet& rAttrSet = pTextFormatCollection->GetAttrSet(); if (rAttrSet.GetLanguage(false).GetLanguage() == LANGUAGE_NONE) { OUString sName = pTextFormatCollection->GetName(); OUString sIssueText = SwResId(STR_STYLE_NO_LANGUAGE).replaceAll("%STYLE_NAME%", sName); lclAddIssue(m_rIssueCollection, sIssueText, sfx::AccessibilityIssueID::STYLE_LANGUAGE); } } } } }; class DocumentTitleCheck : public DocumentCheck { public: DocumentTitleCheck(sfx::AccessibilityIssueCollection& rIssueCollection) : DocumentCheck(rIssueCollection) { } void check(SwDoc* pDoc) override { SwDocShell* pShell = pDoc->GetDocShell(); if (pShell) { const uno::Reference xDPS(pShell->GetModel(), uno::UNO_QUERY_THROW); const uno::Reference xDocumentProperties( xDPS->getDocumentProperties()); OUString sTitle = xDocumentProperties->getTitle(); if (sTitle.isEmpty()) { lclAddIssue(m_rIssueCollection, SwResId(STR_DOCUMENT_TITLE), sfx::AccessibilityIssueID::DOCUMENT_TITLE); } } } }; class FootnoteEndnoteCheck : public DocumentCheck { public: FootnoteEndnoteCheck(sfx::AccessibilityIssueCollection& rIssueCollection) : DocumentCheck(rIssueCollection) { } void check(SwDoc* pDoc) override { for (SwTextFootnote const* pTextFootnote : pDoc->GetFootnoteIdxs()) { SwFormatFootnote const& rFootnote = pTextFootnote->GetFootnote(); if (rFootnote.IsEndNote()) { lclAddIssue(m_rIssueCollection, SwResId(STR_AVOID_ENDNOTES)); } else { lclAddIssue(m_rIssueCollection, SwResId(STR_AVOID_FOOTNOTES)); } } } }; } // end anonymous namespace // Check Shapes, TextBox void AccessibilityCheck::checkObject(SdrObject* pObject) { if (!pObject) return; if (pObject->GetObjIdentifier() == OBJ_CUSTOMSHAPE || pObject->GetObjIdentifier() == OBJ_TEXT) { OUString sAlternative = pObject->GetTitle(); if (sAlternative.isEmpty()) { OUString sName = pObject->GetName(); OUString sIssueText = SwResId(STR_NO_ALT).replaceAll("%OBJECT_NAME%", sName); lclAddIssue(m_aIssueCollection, sIssueText, sfx::AccessibilityIssueID::NO_ALT_SHAPE); } } } void AccessibilityCheck::check() { if (m_pDoc == nullptr) return; std::vector> aDocumentChecks; aDocumentChecks.push_back(std::make_unique(m_aIssueCollection)); aDocumentChecks.push_back(std::make_unique(m_aIssueCollection)); aDocumentChecks.push_back(std::make_unique(m_aIssueCollection)); for (std::unique_ptr& rpDocumentCheck : aDocumentChecks) { rpDocumentCheck->check(m_pDoc); } std::vector> aNodeChecks; aNodeChecks.push_back(std::make_unique(m_aIssueCollection)); aNodeChecks.push_back(std::make_unique(m_aIssueCollection)); aNodeChecks.push_back(std::make_unique(m_aIssueCollection)); aNodeChecks.push_back(std::make_unique(m_aIssueCollection)); aNodeChecks.push_back(std::make_unique(m_aIssueCollection)); aNodeChecks.push_back(std::make_unique(m_aIssueCollection)); aNodeChecks.push_back(std::make_unique(m_aIssueCollection)); aNodeChecks.push_back(std::make_unique(m_aIssueCollection)); auto const& pNodes = m_pDoc->GetNodes(); SwNode* pNode = nullptr; for (sal_uLong n = 0; n < pNodes.Count(); ++n) { pNode = pNodes[n]; if (pNode) { for (std::unique_ptr& rpNodeCheck : aNodeChecks) { rpNodeCheck->check(pNode); } } } IDocumentDrawModelAccess& rDrawModelAccess = m_pDoc->getIDocumentDrawModelAccess(); auto* pModel = rDrawModelAccess.GetDrawModel(); for (sal_uInt16 nPage = 0; nPage < pModel->GetPageCount(); ++nPage) { SdrPage* pPage = pModel->GetPage(nPage); for (size_t nObject = 0; nObject < pPage->GetObjCount(); ++nObject) { SdrObject* pObject = pPage->GetObj(nObject); if (pObject) checkObject(pObject); } } } } // end sw namespace /* vim:set shiftwidth=4 softtabstop=4 expandtab: */