diff --git a/CMakeLists.txt b/CMakeLists.txt index 38f1374..b739f34 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,105 +1,108 @@ cmake_minimum_required(VERSION 3.0) set(PIM_VERSION "5.9.45") project(KItinerary VERSION ${PIM_VERSION}) set(KF5_VERSION "5.48.0") find_package(ECM ${KF5_VERSION} REQUIRED NO_MODULE) set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH} ${ECM_KDE_MODULE_DIR} ${CMAKE_SOURCE_DIR}/cmake) include(KDEFrameworkCompilerSettings NO_POLICY_SCOPE) include(ECMAddTests) include(ECMGenerateHeaders) include(ECMQtDeclareLoggingCategory) include(ECMSetupVersion) include(FeatureSummary) include(KDEInstallDirs) include(KDECMakeSettings) include(GenerateExportHeader) ecm_setup_version(PROJECT VARIABLE_PREFIX KITINERARY VERSION_HEADER kitinerary_version.h PACKAGE_VERSION_FILE "${CMAKE_CURRENT_BINARY_DIR}/KPimItineraryConfigVersion.cmake" ) set(QT_REQUIRED_VERSION "5.9.0") find_package(Qt5 ${QT_REQUIRED_VERSION} REQUIRED COMPONENTS Test Gui Qml) find_package(KF5 ${KF5_VERSION} REQUIRED COMPONENTS I18n) set(KMIME_VERSION "5.9.40") set(KCALENDARCORE_LIB_VERSION "5.9.40") set(KCONTACTS_LIB_VERSION "5.9.40") find_package(KF5Mime ${KMIME_VERSION} CONFIG REQUIRED) find_package(KF5CalendarCore ${KCALENDARCORE_LIB_VERSION} CONFIG) find_package(KF5Contacts ${KCONTACTS_LIB_VERSION} CONFIG) if (NOT ANDROID) set_package_properties(KF5CalendarCore PROPERTIES TYPE REQUIRED) set_package_properties(KF5Contacts PROPERTIES TYPE REQUIRED) endif() find_package(KPimPkPass CONFIG REQUIRED) find_package(Poppler COMPONENTS Core) set_package_properties("Poppler" PROPERTIES TYPE OPTIONAL PURPOSE "Support for extraction from PDF booking confirmations.") find_package(zxing CONFIG) set_package_properties("zxing" PROPERTIES TYPE OPTIONAL PURPOSE "Support for barcode decoding." URL "https://github.com/glassechidna/zxing-cpp") find_package(ZLIB REQUIRED) set_package_properties("ZLIB" PROPERTIES PURPOSE "Support for decoding UIC 918-3 train tickets.") find_package(LibXml2 MODULE) set_package_properties("LibXml2" PROPERTIES PURPOSE "Support for extraction from HTML booking confirmations." URL "http://libxml.org") if(TARGET Poppler::Core) set(HAVE_POPPLER ON) if (${Poppler_VERSION} VERSION_GREATER 0.57) set(HAVE_POPPLER_0_58 ON) endif() + if (${Poppler_VERSION} VERSION_GREATER 0.68) + set(HAVE_POPPLER_0_69 ON) + endif() endif() if (TARGET zxing::libzxing) set(HAVE_ZXING ON) endif() if (TARGET KF5::CalendarCore) set(HAVE_KCAL ON) endif() if (TARGET KF5::Contacts) set(HAVE_KCONTACTS ON) endif() if (LIBXML2_FOUND) set(HAVE_LIBXML2 ON) endif() add_definitions(-DTRANSLATION_DOMAIN=\"kitinerary\") add_definitions(-DQT_NO_CAST_FROM_ASCII) add_definitions(-DQT_NO_CAST_TO_ASCII) add_definitions(-DQT_NO_NARROWING_CONVERSIONS_IN_CONNECT) add_definitions(-DQT_NO_URL_CAST_FROM_STRING) add_definitions(-DQT_USE_QSTRINGBUILDER) add_definitions(-DQT_DISABLE_DEPRECATED_BEFORE=0x060000) add_subdirectory(src) if(BUILD_TESTING) add_subdirectory(autotests) endif() feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES) set(CMAKECONFIG_INSTALL_DIR "${CMAKECONFIG_INSTALL_PREFIX}/KPimItinerary") configure_package_config_file( "${CMAKE_CURRENT_SOURCE_DIR}/KPimItineraryConfig.cmake.in" "${CMAKE_CURRENT_BINARY_DIR}/KPimItineraryConfig.cmake" INSTALL_DESTINATION "${CMAKECONFIG_INSTALL_DIR}" ) install(FILES "${CMAKE_CURRENT_BINARY_DIR}/KPimItineraryConfig.cmake" "${CMAKE_CURRENT_BINARY_DIR}/KPimItineraryConfigVersion.cmake" DESTINATION "${CMAKECONFIG_INSTALL_DIR}" COMPONENT Devel) install(EXPORT KPimItineraryTargets DESTINATION "${CMAKECONFIG_INSTALL_DIR}" FILE KPimItineraryTargets.cmake NAMESPACE KPim:: ) install(FILES org_kde_kitinerary.categories DESTINATION ${KDE_INSTALL_CONFDIR}) diff --git a/src/config-kitinerary.h.cmake b/src/config-kitinerary.h.cmake index 139402b..621c08c 100644 --- a/src/config-kitinerary.h.cmake +++ b/src/config-kitinerary.h.cmake @@ -1,30 +1,31 @@ /* Copyright (c) 2017 Volker Krause This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef CONFIG_KITINERARY_H #define CONFIG_KITINERARY_H #cmakedefine HAVE_POPPLER #cmakedefine HAVE_POPPLER_0_58 +#cmakedefine HAVE_POPPLER_0_69 #cmakedefine HAVE_ZXING #cmakedefine HAVE_KCAL #cmakedefine HAVE_KCONTACTS #cmakedefine HAVE_LIBXML2 #endif diff --git a/src/pdfdocument.cpp b/src/pdfdocument.cpp index f72f386..a214dda 100644 --- a/src/pdfdocument.cpp +++ b/src/pdfdocument.cpp @@ -1,484 +1,481 @@ /* Copyright (C) 2018 Volker Krause This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "config-kitinerary.h" #include "pdfdocument.h" #include #include #include #ifdef HAVE_POPPLER #include #include #include #include #endif -// for direct image extraction we need https://bugs.freedesktop.org/show_bug.cgi?id=107617 -// #define HAVE_UNRELEASED_POPPLER 1 - #include using namespace KItinerary; namespace KItinerary { class PdfImagePrivate : public QSharedData { public: #ifdef HAVE_POPPLER void load(Stream *str, GfxImageColorMap *colorMap); std::unique_ptr m_colorMap; #endif int m_refNum = -1; int m_refGen = -1; PdfPagePrivate *m_page = nullptr; QImage m_img; double m_x = NAN; double m_y = NAN; int m_width = 0; int m_height = 0; int m_sourceWidth = 0; int m_sourceHeight = 0; QImage::Format m_format = QImage::Format_Invalid; }; class PdfPagePrivate : public QSharedData { public: int m_pageNum = -1; QString m_text; std::vector m_images; PdfDocumentPrivate *m_doc; }; class PdfDocumentPrivate { public: QByteArray m_pdfData; // needs to be kept alive as long as the Poppler::PdfDoc instance lives std::vector m_images; std::vector m_pages; #ifdef HAVE_POPPLER std::unique_ptr m_popplerDoc; #endif }; #ifdef HAVE_POPPLER static std::unique_ptr s_globalParams; static GlobalParams* popplerGlobalParams() { if (!s_globalParams) { s_globalParams.reset(new GlobalParams); } return s_globalParams.get(); } class ExtractorOutputDevice : public TextOutputDev { public: ExtractorOutputDevice(PdfDocumentPrivate *dd); GBool needNonText() override { return true; } void drawImage(GfxState *state, Object *ref, Stream *str, int width, int height, GfxImageColorMap *colorMap, GBool interpolate, int *maskColors, GBool inlineImg) override; std::vector m_images; private: PdfDocumentPrivate *d; }; -#ifndef HAVE_UNRELEASED_POPPLER +#ifndef HAVE_POPPLER_0_69 class ImageLoaderOutputDevice : public OutputDev { public: ImageLoaderOutputDevice(PdfImagePrivate *dd); GBool interpretType3Chars() override { return false; } GBool needNonText() override { return true; } GBool upsideDown() override { return false; } GBool useDrawChar() override { return false; } void drawImage(GfxState *state, Object *ref, Stream *str, int width, int height, GfxImageColorMap *colorMap, GBool interpolate, int *maskColors, GBool inlineImg) override; private: PdfImagePrivate *d; }; #endif #endif } #ifdef HAVE_POPPLER void PdfImagePrivate::load(Stream* str, GfxImageColorMap* colorMap) { m_img = QImage(m_sourceWidth, m_sourceHeight, m_format); const auto bytesPerPixel = colorMap->getNumPixelComps(); std::unique_ptr imgStream(new ImageStream(str, m_sourceWidth, bytesPerPixel, colorMap->getBits())); imgStream->reset(); switch (m_format) { case QImage::Format_RGB888: for (int i = 0; i < m_sourceHeight; ++i) { const auto row = imgStream->getLine(); auto imgData = m_img.scanLine(i); GfxRGB rgb; for (int j = 0; j < m_sourceWidth; ++j) { colorMap->getRGB(row + (j * bytesPerPixel), &rgb); *imgData++ = colToByte(rgb.r); *imgData++ = colToByte(rgb.g); *imgData++ = colToByte(rgb.b); } } break; case QImage::Format_Grayscale8: for (int i = 0; i < m_sourceHeight; ++i) { const auto row = imgStream->getLine(); auto imgData = m_img.scanLine(i); GfxGray gray; for (int j = 0; j < m_sourceWidth; ++j) { colorMap->getGray(row + j, &gray); *imgData++ = colToByte(gray); } } break; default: break; } imgStream->close(); if (m_width != m_sourceWidth || m_height != m_sourceHeight) { m_img = m_img.scaled(m_width, m_height); } } ExtractorOutputDevice::ExtractorOutputDevice(PdfDocumentPrivate *dd) : TextOutputDev(nullptr, false, 0, false, false) , d(dd) { } void ExtractorOutputDevice::drawImage(GfxState* state, Object* ref, Stream* str, int width, int height, GfxImageColorMap* colorMap, GBool interpolate, int* maskColors, GBool inlineImg) { Q_UNUSED(str); Q_UNUSED(interpolate); Q_UNUSED(maskColors); Q_UNUSED(inlineImg); if (!colorMap || !colorMap->isOk()) { return; } // check for duplicate occurances of ref->getRef().num if (ref->isRef()) { auto it = std::find_if(d->m_images.begin(), d->m_images.end(), [ref](const PdfImage &other) { return other.d->m_refNum == ref->getRef().num; }); if (it != d->m_images.end()) { m_images.push_back(*it); return; } } QImage::Format format; if (colorMap->getColorSpace()->getMode() == csIndexed) { format = QImage::Format_RGB888; } else if (colorMap->getNumPixelComps() == 1 && (colorMap->getBits() >= 1 && colorMap->getBits() <= 8)) { format = QImage::Format_Grayscale8; } else if (colorMap->getNumPixelComps() == 3 && colorMap->getBits() == 8) { format = QImage::Format_RGB888; } else { return; } PdfImage pdfImg; if (ref->isRef()) { pdfImg.d->m_refNum = ref->getRef().num; pdfImg.d->m_refGen = ref->getRef().gen; } -#ifdef HAVE_UNRELEASED_POPPLER +#ifdef HAVE_POPPLER_0_69 pdfImg.d->m_colorMap.reset(colorMap->copy()); #endif pdfImg.d->m_sourceHeight = height; pdfImg.d->m_sourceWidth = width; pdfImg.d->m_width = width; pdfImg.d->m_height = height; // deal with aspect-ratio changing scaling const auto sourceAspectRatio = (double)width / (double)height; const auto targetAspectRatio = state->getCTM()[0] / -state->getCTM()[3]; if (!qFuzzyCompare(sourceAspectRatio, targetAspectRatio) && qFuzzyIsNull(state->getCTM()[1]) && qFuzzyIsNull(state->getCTM()[2])) { if (targetAspectRatio > sourceAspectRatio) { pdfImg.d->m_width = height * state->getCTM()[0] / -state->getCTM()[3]; } else { pdfImg.d->m_height = width * -state->getCTM()[3] / state->getCTM()[0]; } } pdfImg.d->m_x = state->getCTM()[4]; pdfImg.d->m_y = state->getCTM()[5]; pdfImg.d->m_format = format; m_images.push_back(pdfImg); } -#ifndef HAVE_UNRELEASED_POPPLER +#ifndef HAVE_POPPLER_0_69 ImageLoaderOutputDevice::ImageLoaderOutputDevice(PdfImagePrivate* dd) : d(dd) { } void ImageLoaderOutputDevice::drawImage(GfxState *state, Object *ref, Stream *str, int width, int height, GfxImageColorMap *colorMap, GBool interpolate, int *maskColors, GBool inlineImg) { Q_UNUSED(state); Q_UNUSED(height); Q_UNUSED(width); Q_UNUSED(interpolate); Q_UNUSED(maskColors); Q_UNUSED(inlineImg); if (!colorMap || !colorMap->isOk()) { return; } if (ref->isRef() && d->m_refNum != ref->getRef().num) { return; } d->load(str, colorMap); } #endif #endif PdfImage::PdfImage() : d(new PdfImagePrivate) { } PdfImage::PdfImage(const PdfImage&) = default; PdfImage::~PdfImage() = default; PdfImage& PdfImage::operator=(const PdfImage&) = default; int PdfImage::height() const { return d->m_height; } int PdfImage::width() const { return d->m_width; } QImage PdfImage::image() const { if (!d->m_img.isNull() || d->m_refNum < 0) { return d->m_img; } #ifdef HAVE_POPPLER QScopedValueRollback globalParamResetter(globalParams, popplerGlobalParams()); -#ifdef HAVE_UNRELEASED_POPPLER +#ifdef HAVE_POPPLER_0_69 const auto xref = d->m_page->m_doc->m_popplerDoc->getXRef(); const auto obj = xref->fetch(d->m_refNum, d->m_refGen); d->load(obj.getStream(), d->m_colorMap.get()); #else std::unique_ptr device(new ImageLoaderOutputDevice(d.data())); d->m_page->m_doc->m_popplerDoc->displayPageSlice(device.get(), d->m_page->m_pageNum + 1, 72, 72, 0, false, true, false, -1, -1, -1, -1); #endif #endif return d->m_img; } PdfPage::PdfPage() : d(new PdfPagePrivate) { } PdfPage::PdfPage(const PdfPage&) = default; PdfPage::~PdfPage() = default; PdfPage& PdfPage::operator=(const PdfPage&) = default; QString PdfPage::text() const { return d->m_text; } #ifdef HAVE_POPPLER static double ratio(double begin, double end, double ratio) { return begin + (end - begin) * ratio; } #endif QString PdfPage::textInRect(double left, double top, double right, double bottom) const { #ifdef HAVE_POPPLER QScopedValueRollback globalParamResetter(globalParams, popplerGlobalParams()); std::unique_ptr device(new ExtractorOutputDevice(d->m_doc)); d->m_doc->m_popplerDoc->displayPageSlice(device.get(), d->m_pageNum + 1, 72, 72, 0, false, true, false, -1, -1, -1, -1); const auto pageRect = d->m_doc->m_popplerDoc->getPage(d->m_pageNum + 1)->getCropBox(); std::unique_ptr s(device->getText(ratio(pageRect->x1, pageRect->x2, left), ratio(pageRect->y1, pageRect->y2, top), ratio(pageRect->x1, pageRect->x2, right), ratio(pageRect->y1, pageRect->y2, bottom))); return QString::fromUtf8(s->getCString()); #else Q_UNUSED(left); Q_UNUSED(top); Q_UNUSED(right); Q_UNUSED(bottom); return {}; #endif } int PdfPage::imageCount() const { return d->m_images.size(); } PdfImage PdfPage::image(int index) const { return d->m_images[index]; } QVariantList PdfPage::imagesVariant() const { QVariantList l; l.reserve(imageCount()); std::for_each(d->m_images.begin(), d->m_images.end(), [&l](const PdfImage& img) { l.push_back(QVariant::fromValue(img)); }); return l; } QVariantList PdfPage::imagesInRect(double left, double top, double right, double bottom) const { QVariantList l; #ifdef HAVE_POPPLER QScopedValueRollback globalParamResetter(globalParams, popplerGlobalParams()); const auto pageRect = d->m_doc->m_popplerDoc->getPage(d->m_pageNum + 1)->getCropBox(); for (const auto &img : d->m_images) { if ((img.d->m_x >= ratio(pageRect->x1, pageRect->x2, left) && img.d->m_x <= ratio(pageRect->x1, pageRect->x2, right)) && (img.d->m_y >= ratio(pageRect->y1, pageRect->y2, top) && img.d->m_y <= ratio(pageRect->y1, pageRect->y2, bottom))) { l.push_back(QVariant::fromValue(img)); } } #else Q_UNUSED(left); Q_UNUSED(top); Q_UNUSED(right); Q_UNUSED(bottom); #endif return l; } PdfDocument::PdfDocument(QObject *parent) : QObject(parent) , d(new PdfDocumentPrivate) { } PdfDocument::~PdfDocument() = default; QString PdfDocument::text() const { QString text; std::for_each(d->m_pages.begin(), d->m_pages.end(), [&text](const PdfPage &p) { text += p.text(); }); return text; } int PdfDocument::imageCount() const { return d->m_images.size(); } PdfImage PdfDocument::image(int index) const { return d->m_images[index]; } QVariantList PdfDocument::imagesVariant() const { QVariantList l; l.reserve(imageCount()); std::for_each(d->m_images.begin(), d->m_images.end(), [&l](const PdfImage& img) { l.push_back(QVariant::fromValue(img)); }); return l; } int PdfDocument::pageCount() const { return d->m_pages.size(); } PdfPage PdfDocument::page(int index) const { return d->m_pages[index]; } QVariantList PdfDocument::pagesVariant() const { QVariantList l; l.reserve(pageCount()); std::for_each(d->m_pages.begin(), d->m_pages.end(), [&l](const PdfPage& p) { l.push_back(QVariant::fromValue(p)); }); return l; } PdfDocument* PdfDocument::fromData(const QByteArray &data, QObject *parent) { #ifdef HAVE_POPPLER QScopedValueRollback globalParamResetter(globalParams, popplerGlobalParams()); std::unique_ptr doc(new PdfDocument(parent)); doc->d->m_pdfData = data; // PDFDoc takes ownership of stream #ifdef HAVE_POPPLER_0_58 auto stream = new MemStream(const_cast(doc->d->m_pdfData.constData()), 0, doc->d->m_pdfData.size(), Object()); #else Object obj; obj.initNull(); auto stream = new MemStream(const_cast(doc->d->m_pdfData.constData()), 0, doc->d->m_pdfData.size(), &obj); #endif std::unique_ptr popplerDoc(new PDFDoc(stream, nullptr, nullptr)); if (!popplerDoc->isOk()) { return nullptr; } std::unique_ptr device(new ExtractorOutputDevice(doc->d.get())); doc->d->m_pages.reserve(popplerDoc->getNumPages()); for (int i = 0; i < popplerDoc->getNumPages(); ++i) { popplerDoc->displayPageSlice(device.get(), i + 1, 72, 72, 0, false, true, false, -1, -1, -1, -1); const auto pageRect = popplerDoc->getPage(i + 1)->getCropBox(); std::unique_ptr s(device->getText(pageRect->x1, pageRect->y1, pageRect->x2, pageRect->y2)); std::copy(device->m_images.begin(), device->m_images.end(), std::back_inserter(doc->d->m_images)); PdfPage page; page.d->m_pageNum = i; page.d->m_doc = doc->d.get(); page.d->m_text = QString::fromUtf8(s->getCString()); page.d->m_images = std::move(device->m_images); for (auto it = page.d->m_images.begin(); it != page.d->m_images.end(); ++it) { (*it).d->m_page = page.d.data(); } doc->d->m_pages.push_back(page); } doc->d->m_popplerDoc = std::move(popplerDoc); return doc.release(); #else Q_UNUSED(data); Q_UNUSED(parent); return nullptr; #endif }