diff options
Diffstat (limited to '')
-rw-r--r-- | vcl/qa/cppunit/PDFiumLibraryTest.cxx | 454 |
1 files changed, 454 insertions, 0 deletions
diff --git a/vcl/qa/cppunit/PDFiumLibraryTest.cxx b/vcl/qa/cppunit/PDFiumLibraryTest.cxx new file mode 100644 index 0000000000..966c44a3f5 --- /dev/null +++ b/vcl/qa/cppunit/PDFiumLibraryTest.cxx @@ -0,0 +1,454 @@ +/* -*- 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 <string_view> + +#include <cppunit/TestAssert.h> +#include <cppunit/extensions/HelperMacros.h> + +#include <unotest/bootstrapfixturebase.hxx> +#include <unotest/directories.hxx> +#include <unotools/datetime.hxx> + +#include <com/sun/star/util/DateTime.hpp> + +#include <vcl/graph.hxx> +#include <vcl/graphicfilter.hxx> +#include <tools/stream.hxx> + +#include <vcl/filter/PDFiumLibrary.hxx> +#include <vcl/pdfread.hxx> +#include <vcl/BitmapReadAccess.hxx> + +class PDFiumLibraryTest : public test::BootstrapFixtureBase +{ + OUString getFullUrl(std::u16string_view sFileName) + { + return m_directories.getURLFromSrc(u"/vcl/qa/cppunit/data/") + sFileName; + } + + void testDocument(); + void testPages(); + void testPageObjects(); + void testAnnotationsMadeInEvince(); + void testAnnotationsMadeInAcrobat(); + void testAnnotationsDifferentTypes(); + void testTools(); + void testFormFields(); + + CPPUNIT_TEST_SUITE(PDFiumLibraryTest); + CPPUNIT_TEST(testDocument); + CPPUNIT_TEST(testPages); + CPPUNIT_TEST(testPageObjects); + CPPUNIT_TEST(testAnnotationsMadeInEvince); + CPPUNIT_TEST(testAnnotationsMadeInAcrobat); + CPPUNIT_TEST(testAnnotationsDifferentTypes); + CPPUNIT_TEST(testTools); + CPPUNIT_TEST(testFormFields); + CPPUNIT_TEST_SUITE_END(); +}; + +void PDFiumLibraryTest::testDocument() +{ + OUString aURL = getFullUrl(u"Pangram.pdf"); + SvFileStream aStream(aURL, StreamMode::READ); + GraphicFilter& rGraphicFilter = GraphicFilter::GetGraphicFilter(); + Graphic aGraphic = rGraphicFilter.ImportUnloadedGraphic(aStream); + aGraphic.makeAvailable(); + + auto pVectorGraphicData = aGraphic.getVectorGraphicData(); + CPPUNIT_ASSERT(pVectorGraphicData); + CPPUNIT_ASSERT_EQUAL(VectorGraphicDataType::Pdf, pVectorGraphicData->getType()); + + auto& rDataContainer = pVectorGraphicData->getBinaryDataContainer(); + + auto pPdfium = vcl::pdf::PDFiumLibrary::get(); + CPPUNIT_ASSERT(pPdfium); + auto pDocument + = pPdfium->openDocument(rDataContainer.getData(), rDataContainer.getSize(), OString()); + CPPUNIT_ASSERT(pDocument); + + CPPUNIT_ASSERT_EQUAL(1, pDocument->getPageCount()); + + auto aSize = pDocument->getPageSize(0); + CPPUNIT_ASSERT_EQUAL(612.0, aSize.getWidth()); + CPPUNIT_ASSERT_EQUAL(792.0, aSize.getHeight()); +} + +void PDFiumLibraryTest::testPages() +{ + OUString aURL = getFullUrl(u"Pangram.pdf"); + SvFileStream aStream(aURL, StreamMode::READ); + GraphicFilter& rGraphicFilter = GraphicFilter::GetGraphicFilter(); + Graphic aGraphic = rGraphicFilter.ImportUnloadedGraphic(aStream); + aGraphic.makeAvailable(); + + auto pVectorGraphicData = aGraphic.getVectorGraphicData(); + CPPUNIT_ASSERT(pVectorGraphicData); + CPPUNIT_ASSERT_EQUAL(VectorGraphicDataType::Pdf, pVectorGraphicData->getType()); + + auto& rDataContainer = pVectorGraphicData->getBinaryDataContainer(); + + auto pPdfium = vcl::pdf::PDFiumLibrary::get(); + auto pDocument + = pPdfium->openDocument(rDataContainer.getData(), rDataContainer.getSize(), OString()); + CPPUNIT_ASSERT(pDocument); + + CPPUNIT_ASSERT_EQUAL(1, pDocument->getPageCount()); + + auto pPage = pDocument->openPage(0); + CPPUNIT_ASSERT(pPage); +} + +void PDFiumLibraryTest::testPageObjects() +{ + OUString aURL = getFullUrl(u"Pangram.pdf"); + SvFileStream aStream(aURL, StreamMode::READ); + GraphicFilter& rGraphicFilter = GraphicFilter::GetGraphicFilter(); + Graphic aGraphic = rGraphicFilter.ImportUnloadedGraphic(aStream); + aGraphic.makeAvailable(); + + auto pVectorGraphicData = aGraphic.getVectorGraphicData(); + CPPUNIT_ASSERT(pVectorGraphicData); + CPPUNIT_ASSERT_EQUAL(VectorGraphicDataType::Pdf, pVectorGraphicData->getType()); + + auto& rDataContainer = pVectorGraphicData->getBinaryDataContainer(); + + auto pPdfium = vcl::pdf::PDFiumLibrary::get(); + auto pDocument + = pPdfium->openDocument(rDataContainer.getData(), rDataContainer.getSize(), OString()); + CPPUNIT_ASSERT(pDocument); + + CPPUNIT_ASSERT_EQUAL(1, pDocument->getPageCount()); + + auto pPage = pDocument->openPage(0); + CPPUNIT_ASSERT(pPage); + + CPPUNIT_ASSERT_EQUAL(12, pPage->getObjectCount()); + + auto pPageObject = pPage->getObject(0); + auto pTextPage = pPage->getTextPage(); + + CPPUNIT_ASSERT_EQUAL(vcl::pdf::PDFPageObjectType::Text, pPageObject->getType()); + + CPPUNIT_ASSERT_EQUAL(OUString("The quick, brown fox jumps over a lazy dog. DJs flock by when " + "MTV ax quiz prog. Junk MTV quiz "), + pPageObject->getText(pTextPage)); + + CPPUNIT_ASSERT_EQUAL(12.0, pPageObject->getFontSize()); + CPPUNIT_ASSERT_EQUAL(OUString("Liberation Serif"), pPageObject->getFontName()); + CPPUNIT_ASSERT_EQUAL(vcl::pdf::PDFTextRenderMode::Fill, pPageObject->getTextRenderMode()); + CPPUNIT_ASSERT_EQUAL(COL_BLACK, pPageObject->getFillColor()); + CPPUNIT_ASSERT_EQUAL(COL_BLACK, pPageObject->getStrokeColor()); + + basegfx::B2DHomMatrix aMatrix = pPageObject->getMatrix(); + // Ignore translation, ensure there is no rotate/scale. + aMatrix.set(0, 2, 0); + aMatrix.set(1, 2, 0); + CPPUNIT_ASSERT_EQUAL(true, aMatrix.isIdentity()); + + CPPUNIT_ASSERT_DOUBLES_EQUAL(057.01, pPageObject->getBounds().getMinX(), 1E-2); + CPPUNIT_ASSERT_DOUBLES_EQUAL(721.51, pPageObject->getBounds().getMinY(), 1E-2); + CPPUNIT_ASSERT_DOUBLES_EQUAL(539.48, pPageObject->getBounds().getMaxX(), 1E-2); + CPPUNIT_ASSERT_DOUBLES_EQUAL(732.54, pPageObject->getBounds().getMaxY(), 1E-2); +} + +void PDFiumLibraryTest::testAnnotationsMadeInEvince() +{ + OUString aURL = getFullUrl(u"PangramWithAnnotations.pdf"); + SvFileStream aStream(aURL, StreamMode::READ); + GraphicFilter& rGraphicFilter = GraphicFilter::GetGraphicFilter(); + Graphic aGraphic = rGraphicFilter.ImportUnloadedGraphic(aStream); + aGraphic.makeAvailable(); + + auto pVectorGraphicData = aGraphic.getVectorGraphicData(); + CPPUNIT_ASSERT(pVectorGraphicData); + CPPUNIT_ASSERT_EQUAL(VectorGraphicDataType::Pdf, pVectorGraphicData->getType()); + + auto& rDataContainer = pVectorGraphicData->getBinaryDataContainer(); + + auto pPdfium = vcl::pdf::PDFiumLibrary::get(); + auto pDocument + = pPdfium->openDocument(rDataContainer.getData(), rDataContainer.getSize(), OString()); + CPPUNIT_ASSERT(pDocument); + + CPPUNIT_ASSERT_EQUAL(1, pDocument->getPageCount()); + + auto pPage = pDocument->openPage(0); + CPPUNIT_ASSERT(pPage); + + CPPUNIT_ASSERT_EQUAL(2, pPage->getAnnotationCount()); + + { + auto pAnnotation = pPage->getAnnotation(0); + CPPUNIT_ASSERT(pAnnotation); + CPPUNIT_ASSERT_EQUAL(vcl::pdf::PDFAnnotationSubType::Text, pAnnotation->getSubType()); + + OUString aPopupString = pAnnotation->getString(vcl::pdf::constDictionaryKeyTitle); + CPPUNIT_ASSERT_EQUAL(OUString("quikee"), aPopupString); + + OUString aContentsString = pAnnotation->getString(vcl::pdf::constDictionaryKeyContents); + CPPUNIT_ASSERT_EQUAL(OUString("Annotation test"), aContentsString); + + CPPUNIT_ASSERT_EQUAL(true, pAnnotation->hasKey(vcl::pdf::constDictionaryKeyPopup)); + auto pPopupAnnotation = pAnnotation->getLinked(vcl::pdf::constDictionaryKeyPopup); + CPPUNIT_ASSERT(pPopupAnnotation); + + CPPUNIT_ASSERT_EQUAL(1, pPage->getAnnotationIndex(pPopupAnnotation)); + CPPUNIT_ASSERT_EQUAL(vcl::pdf::PDFAnnotationSubType::Popup, pPopupAnnotation->getSubType()); + + OUString sDateTimeString + = pAnnotation->getString(vcl::pdf::constDictionaryKeyModificationDate); + CPPUNIT_ASSERT_EQUAL(OUString("D:20200612201322+02'00"), sDateTimeString); + } + + { + auto pAnnotation = pPage->getAnnotation(1); + CPPUNIT_ASSERT(pAnnotation); + CPPUNIT_ASSERT_EQUAL(vcl::pdf::PDFAnnotationSubType::Popup, pAnnotation->getSubType()); + } +} + +void PDFiumLibraryTest::testAnnotationsMadeInAcrobat() +{ + OUString aURL = getFullUrl(u"PangramAcrobatAnnotations.pdf"); + SvFileStream aStream(aURL, StreamMode::READ); + GraphicFilter& rGraphicFilter = GraphicFilter::GetGraphicFilter(); + Graphic aGraphic = rGraphicFilter.ImportUnloadedGraphic(aStream); + aGraphic.makeAvailable(); + + auto pVectorGraphicData = aGraphic.getVectorGraphicData(); + CPPUNIT_ASSERT(pVectorGraphicData); + CPPUNIT_ASSERT_EQUAL(VectorGraphicDataType::Pdf, pVectorGraphicData->getType()); + + auto& rDataContainer = pVectorGraphicData->getBinaryDataContainer(); + + auto pPdfium = vcl::pdf::PDFiumLibrary::get(); + auto pDocument + = pPdfium->openDocument(rDataContainer.getData(), rDataContainer.getSize(), OString()); + CPPUNIT_ASSERT(pDocument); + + CPPUNIT_ASSERT_EQUAL(1, pDocument->getPageCount()); + + auto pPage = pDocument->openPage(0); + CPPUNIT_ASSERT(pPage); + + CPPUNIT_ASSERT_EQUAL(4, pPage->getAnnotationCount()); + + { + auto pAnnotation = pPage->getAnnotation(0); + CPPUNIT_ASSERT(pAnnotation); + CPPUNIT_ASSERT_EQUAL(vcl::pdf::PDFAnnotationSubType::Text, pAnnotation->getSubType()); + + OUString aPopupString = pAnnotation->getString(vcl::pdf::constDictionaryKeyTitle); + CPPUNIT_ASSERT_EQUAL(OUString("quikee"), aPopupString); + + OUString aContentsString = pAnnotation->getString(vcl::pdf::constDictionaryKeyContents); + CPPUNIT_ASSERT_EQUAL(OUString("YEEEY"), aContentsString); + + CPPUNIT_ASSERT_EQUAL(true, pAnnotation->hasKey(vcl::pdf::constDictionaryKeyPopup)); + auto pPopupAnnotation = pAnnotation->getLinked(vcl::pdf::constDictionaryKeyPopup); + CPPUNIT_ASSERT(pPopupAnnotation); + + CPPUNIT_ASSERT_EQUAL(1, pPage->getAnnotationIndex(pPopupAnnotation)); + CPPUNIT_ASSERT_EQUAL(vcl::pdf::PDFAnnotationSubType::Popup, pPopupAnnotation->getSubType()); + } + + { + auto pAnnotation = pPage->getAnnotation(1); + CPPUNIT_ASSERT(pAnnotation); + CPPUNIT_ASSERT_EQUAL(vcl::pdf::PDFAnnotationSubType::Popup, pAnnotation->getSubType()); + } + + { + auto pAnnotation = pPage->getAnnotation(2); + CPPUNIT_ASSERT(pAnnotation); + CPPUNIT_ASSERT_EQUAL(vcl::pdf::PDFAnnotationSubType::Text, pAnnotation->getSubType()); + + OUString aPopupString = pAnnotation->getString(vcl::pdf::constDictionaryKeyTitle); + CPPUNIT_ASSERT_EQUAL(OUString("quikee"), aPopupString); + + OUString aContentsString = pAnnotation->getString(vcl::pdf::constDictionaryKeyContents); + CPPUNIT_ASSERT_EQUAL(OUString("Note"), aContentsString); + + CPPUNIT_ASSERT_EQUAL(true, pAnnotation->hasKey(vcl::pdf::constDictionaryKeyPopup)); + auto pPopupAnnotation = pAnnotation->getLinked(vcl::pdf::constDictionaryKeyPopup); + CPPUNIT_ASSERT(pPopupAnnotation); + + CPPUNIT_ASSERT_EQUAL(3, pPage->getAnnotationIndex(pPopupAnnotation)); + CPPUNIT_ASSERT_EQUAL(vcl::pdf::PDFAnnotationSubType::Popup, pPopupAnnotation->getSubType()); + } + + { + auto pAnnotation = pPage->getAnnotation(3); + CPPUNIT_ASSERT(pAnnotation); + CPPUNIT_ASSERT_EQUAL(vcl::pdf::PDFAnnotationSubType::Popup, pAnnotation->getSubType()); + } +} + +void PDFiumLibraryTest::testFormFields() +{ + // Given a document with a form field that looks like plain text: + OUString aURL = getFullUrl(u"form-fields.pdf"); + SvFileStream aFileStream(aURL, StreamMode::READ); + SvMemoryStream aMemory; + aMemory.WriteStream(aFileStream); + aMemory.Seek(0); + + // When rendering its first (and only) page to a bitmap: + std::vector<BitmapEx> aBitmaps; + int nRet = vcl::RenderPDFBitmaps(aMemory.GetData(), aMemory.GetSize(), aBitmaps); + CPPUNIT_ASSERT(nRet); + CPPUNIT_ASSERT_EQUAL(static_cast<size_t>(1), aBitmaps.size()); + + // Then make sure the bitmap contains that text: + Bitmap aBitmap = aBitmaps[0].GetBitmap(); + BitmapReadAccess aAccess(aBitmap); + Size aSize = aBitmap.GetSizePixel(); + std::set<sal_uInt32> aColors; + for (tools::Long y = 0; y < aSize.Height(); ++y) + { + for (tools::Long x = 0; x < aSize.Width(); ++x) + { + aColors.insert(static_cast<sal_uInt32>(aAccess.GetPixel(y, x))); + } + } + // Without the accompanying fix in place, this test would have failed with: + // - Expected greater than: 1 + // - Actual : 1 + // i.e. at least black text and white background is expected (possibly more, due to + // anti-aliasing), but nothing was rendered. + CPPUNIT_ASSERT_GREATER(static_cast<size_t>(1), aColors.size()); +} + +void PDFiumLibraryTest::testAnnotationsDifferentTypes() +{ + OUString aURL = getFullUrl(u"PangramWithMultipleTypeOfAnnotations.pdf"); + SvFileStream aStream(aURL, StreamMode::READ); + GraphicFilter& rGraphicFilter = GraphicFilter::GetGraphicFilter(); + Graphic aGraphic = rGraphicFilter.ImportUnloadedGraphic(aStream); + aGraphic.makeAvailable(); + + auto pVectorGraphicData = aGraphic.getVectorGraphicData(); + CPPUNIT_ASSERT(pVectorGraphicData); + CPPUNIT_ASSERT_EQUAL(VectorGraphicDataType::Pdf, pVectorGraphicData->getType()); + + auto& rDataContainer = pVectorGraphicData->getBinaryDataContainer(); + + auto pPdfium = vcl::pdf::PDFiumLibrary::get(); + auto pDocument + = pPdfium->openDocument(rDataContainer.getData(), rDataContainer.getSize(), OString()); + CPPUNIT_ASSERT(pDocument); + + CPPUNIT_ASSERT_EQUAL(1, pDocument->getPageCount()); + + auto pPage = pDocument->openPage(0); + CPPUNIT_ASSERT(pPage); + + CPPUNIT_ASSERT_EQUAL(6, pPage->getAnnotationCount()); + + { + auto pAnnotation = pPage->getAnnotation(0); + CPPUNIT_ASSERT(pAnnotation); + CPPUNIT_ASSERT_EQUAL(vcl::pdf::PDFAnnotationSubType::FreeText, pAnnotation->getSubType()); + CPPUNIT_ASSERT_EQUAL(0, pAnnotation->getObjectCount()); + OUString aContentsString = pAnnotation->getString(vcl::pdf::constDictionaryKeyContents); + CPPUNIT_ASSERT_EQUAL(OUString("Inline Note"), aContentsString); + auto const& rLineGeometry = pAnnotation->getLineGeometry(); + CPPUNIT_ASSERT_EQUAL(true, rLineGeometry.empty()); + } + + { + auto pAnnotation = pPage->getAnnotation(1); + CPPUNIT_ASSERT(pAnnotation); + CPPUNIT_ASSERT_EQUAL(vcl::pdf::PDFAnnotationSubType::Ink, pAnnotation->getSubType()); + CPPUNIT_ASSERT_EQUAL(0, pAnnotation->getObjectCount()); + OUString aContentsString = pAnnotation->getString(vcl::pdf::constDictionaryKeyContents); + CPPUNIT_ASSERT_EQUAL(OUString("Freehand Text"), aContentsString); + CPPUNIT_ASSERT_EQUAL(size_t(1), pAnnotation->getInkStrokes().size()); + auto const& aInkStrokes = pAnnotation->getInkStrokes(); + auto const& aPoints = aInkStrokes[0]; + CPPUNIT_ASSERT_EQUAL(size_t(74), aPoints.size()); + CPPUNIT_ASSERT_DOUBLES_EQUAL(2.0f, pAnnotation->getBorderWidth(), 1E-2); + auto const& rLineGeometry = pAnnotation->getLineGeometry(); + CPPUNIT_ASSERT_EQUAL(true, rLineGeometry.empty()); + } + + { + auto pAnnotation = pPage->getAnnotation(2); + CPPUNIT_ASSERT(pAnnotation); + CPPUNIT_ASSERT_EQUAL(vcl::pdf::PDFAnnotationSubType::Line, pAnnotation->getSubType()); + CPPUNIT_ASSERT_EQUAL(0, pAnnotation->getObjectCount()); + OUString aContentsString = pAnnotation->getString(vcl::pdf::constDictionaryKeyContents); + CPPUNIT_ASSERT_EQUAL(OUString("Line Text"), aContentsString); + auto const& rLineGeometry = pAnnotation->getLineGeometry(); + CPPUNIT_ASSERT_EQUAL(false, rLineGeometry.empty()); + } + + { + auto pAnnotation = pPage->getAnnotation(3); + CPPUNIT_ASSERT(pAnnotation); + CPPUNIT_ASSERT_EQUAL(vcl::pdf::PDFAnnotationSubType::Polygon, pAnnotation->getSubType()); + CPPUNIT_ASSERT_EQUAL(0, pAnnotation->getObjectCount()); + CPPUNIT_ASSERT_EQUAL(true, pAnnotation->hasKey("Vertices"_ostr)); + OUString aContentsString = pAnnotation->getString(vcl::pdf::constDictionaryKeyContents); + CPPUNIT_ASSERT_EQUAL(OUString("Polygon Text"), aContentsString); + auto const& aVertices = pAnnotation->getVertices(); + CPPUNIT_ASSERT_EQUAL(size_t(3), aVertices.size()); + CPPUNIT_ASSERT_DOUBLES_EQUAL(2.0f, pAnnotation->getBorderWidth(), 1E-2); + auto const& rLineGeometry = pAnnotation->getLineGeometry(); + CPPUNIT_ASSERT_EQUAL(true, rLineGeometry.empty()); + } + + { + auto pAnnotation = pPage->getAnnotation(4); + CPPUNIT_ASSERT(pAnnotation); + CPPUNIT_ASSERT_EQUAL(vcl::pdf::PDFAnnotationSubType::Circle, pAnnotation->getSubType()); + CPPUNIT_ASSERT_EQUAL(0, pAnnotation->getObjectCount()); + OUString aContentsString = pAnnotation->getString(vcl::pdf::constDictionaryKeyContents); + CPPUNIT_ASSERT_EQUAL(OUString("Ellipse Text"), aContentsString); + auto const& rLineGeometry = pAnnotation->getLineGeometry(); + CPPUNIT_ASSERT_EQUAL(true, rLineGeometry.empty()); + } + + { + auto pAnnotation = pPage->getAnnotation(5); + CPPUNIT_ASSERT(pAnnotation); + CPPUNIT_ASSERT_EQUAL(vcl::pdf::PDFAnnotationSubType::Square, pAnnotation->getSubType()); + CPPUNIT_ASSERT_EQUAL(0, pAnnotation->getObjectCount()); + OUString aContentsString = pAnnotation->getString(vcl::pdf::constDictionaryKeyContents); + CPPUNIT_ASSERT_EQUAL(OUString("Rectangle Text"), aContentsString); + CPPUNIT_ASSERT_EQUAL(Color(0xFF, 0xE0, 0x00), pAnnotation->getColor()); + CPPUNIT_ASSERT_EQUAL(false, pAnnotation->hasKey(vcl::pdf::constDictionaryKeyInteriorColor)); + auto const& rLineGeometry = pAnnotation->getLineGeometry(); + CPPUNIT_ASSERT_EQUAL(true, rLineGeometry.empty()); + } +} + +void PDFiumLibraryTest::testTools() +{ + OUString sConverted = vcl::pdf::convertPdfDateToISO8601(u"D:20200612201322+02'00"); + + css::util::DateTime aDateTime; + CPPUNIT_ASSERT(utl::ISO8601parseDateTime(sConverted, aDateTime)); + CPPUNIT_ASSERT_EQUAL(sal_Int16(2020), aDateTime.Year); + CPPUNIT_ASSERT_EQUAL(sal_uInt16(6), aDateTime.Month); + CPPUNIT_ASSERT_EQUAL(sal_uInt16(12), aDateTime.Day); + CPPUNIT_ASSERT_EQUAL(sal_uInt16(20), aDateTime.Hours); + CPPUNIT_ASSERT_EQUAL(sal_uInt16(13), aDateTime.Minutes); + CPPUNIT_ASSERT_EQUAL(sal_uInt16(22), aDateTime.Seconds); + CPPUNIT_ASSERT_EQUAL(sal_uInt32(0), aDateTime.NanoSeconds); + CPPUNIT_ASSERT_EQUAL(false, bool(aDateTime.IsUTC)); +} + +CPPUNIT_TEST_SUITE_REGISTRATION(PDFiumLibraryTest); + +CPPUNIT_PLUGIN_IMPLEMENT(); + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ |