syncfusion / flutter-widgets

Syncfusion Flutter widgets libraries include high quality UI widgets and file-format packages to help you create rich, high-quality applications for iOS, Android, and web from a single code base.
1.6k stars 782 forks source link

a windows rendering implementation for [syncfusion_flutter_pdfviewer] based on pdfium and pdfx #597

Closed insinfo closed 2 years ago

insinfo commented 2 years ago

I implemented syncfusion_flutter_pdfviewer_platform_interface in c++ for windows using pdfium, would you be interested in merging this?

image

syncfusion_flutter_pdf_viewer_plugin.cpp

#include "include/syncfusion_flutter_pdfviewer/syncfusion_flutter_pdf_viewer_plugin.h"

// This must be included before many other Windows headers.
#include <windows.h>

//  Flutter imports flutter
#include <flutter/method_channel.h>
#include <flutter/plugin_registrar_windows.h>
#include <flutter/standard_method_codec.h>

// std imports
#include <iostream>
#include <map>
#include <memory>
#include <sstream>
#include <stdexcept>
#include <vector>
#include <string>
#include <cmath>

// Library linking
#include <pathcch.h>
#pragma comment(lib, "pathcch.lib")

#include <shlwapi.h>
#pragma comment(lib, "shlwapi.lib")

#include "pdfx.h"

namespace pdfx
{

    // Convert a wide Unicode string to an UTF8 string
  std::string utf8_encode(const std::wstring &wstr)
  {
    if (wstr.empty())
      return std::string();
    int size_needed = WideCharToMultiByte(CP_UTF8, 0, &wstr[0], (int)wstr.size(),
                                          NULL, 0, NULL, NULL);
    std::string strTo(size_needed, 0);
    WideCharToMultiByte(CP_UTF8, 0, &wstr[0], (int)wstr.size(), &strTo[0],
                        size_needed, NULL, NULL);
    return strTo;
  }

  class SyncfusionFlutterPdfViewerPlugin : public flutter::Plugin
  {
  public:
    static void RegisterWithRegistrar(flutter::PluginRegistrarWindows *registrar);

    SyncfusionFlutterPdfViewerPlugin();

    virtual ~SyncfusionFlutterPdfViewerPlugin();

  private:
    // Called when a method is called on this plugin's channel from Dart.
    void HandleMethodCall(
        const flutter::MethodCall<flutter::EncodableValue> &method_call,
        std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result);
  };

  // static
  // Registers the SyncfusionFlutterPdfViewerPlugin
  void SyncfusionFlutterPdfViewerPlugin::RegisterWithRegistrar(
      flutter::PluginRegistrarWindows *registrar)
  {
    auto channel =
        std::make_unique<flutter::MethodChannel<flutter::EncodableValue>>(
            registrar->messenger(), "syncfusion_flutter_pdfviewer",
            &flutter::StandardMethodCodec::GetInstance());

    auto plugin = std::make_unique<SyncfusionFlutterPdfViewerPlugin>();

    channel->SetMethodCallHandler(
        [plugin_pointer = plugin.get()](const auto &call, auto result)
        {
          plugin_pointer->HandleMethodCall(call, std::move(result));
        });

    registrar->AddPlugin(std::move(plugin));
  }

  SyncfusionFlutterPdfViewerPlugin::SyncfusionFlutterPdfViewerPlugin() {}

  SyncfusionFlutterPdfViewerPlugin::~SyncfusionFlutterPdfViewerPlugin() {}

  // Invokes the method call operations.
  void SyncfusionFlutterPdfViewerPlugin::HandleMethodCall(
      const flutter::MethodCall<flutter::EncodableValue> &method_call,
      std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result)
  {

    // logger("HandleMethodCall:");
    /// Initializes the PDF Renderer and returns the page count.
    if (method_call.method_name().compare("initializePdfRenderer") == 0)
    {
      // logger("initializePdfRenderer:");
      try
      {
        auto *arguments = std::get_if<flutter::EncodableMap>(method_call.arguments());
        auto bytes_it = arguments->find(flutter::EncodableValue("documentBytes"));
        std::vector<uint8_t> documentBytes;
        if (bytes_it == arguments->end())
        {
          result->Error("SyncfusionFlutterPdfViewerPlugin_exception", "documentBytes is required");
          return;
        }
        documentBytes = std::get<std::vector<uint8_t>>(bytes_it->second);

        std::string currentDocumentID;
        auto document_id_it = arguments->find(flutter::EncodableValue("documentID"));
        if (document_id_it == arguments->end())
        {
          result->Error("SyncfusionFlutterPdfViewerPlugin_exception", "documentID is required");
          return;
        }
        currentDocumentID = std::get<std::string>(document_id_it->second);

        std::shared_ptr<Document> doc = openDocument(documentBytes, currentDocumentID);

        int pageCount = doc->getPageCount();
        std::string pageCountS = std::to_string(pageCount);

        result->Success(flutter::EncodableValue(pageCountS));
      }
      catch (std::exception &e)
      {
        printf("initializePdfRenderer: exception %s", e.what());
        result->Error("SyncfusionFlutterPdfViewerPlugin_exception", e.what());
      }
    }
    /// Close document
    else if (method_call.method_name().compare("closeDocument") == 0)
    {
      // logger("closeDocument:");
      auto id = std::get<std::string>(*method_call.arguments());
      closeDocument(id);
      result->Success();
    }

    // Returns the width collection of rendered pages.
    else if (method_call.method_name().compare("getPagesWidth") == 0)
    {
      // logger("getPagesWidth:");
      try
      {
        auto currentDocumentID = std::get<std::string>(*method_call.arguments());
        auto doc = getDocument(currentDocumentID);
        int pageCount = doc->getPageCount();
        std::vector<double> pagesWidth;
        for (int i = 0; i < pageCount; i++)
        {
          std::shared_ptr<Page> page = openPage(currentDocumentID, i);
          auto details = page->getDetails();
          pagesWidth.push_back(static_cast<double>(details.width));
          // page.reset(); //free
          closePage(page->id);
        }
        result->Success(flutter::EncodableValue(pagesWidth));
      }
      catch (std::exception &e)
      {
        result->Error("SyncfusionFlutterPdfViewerPlugin_exception", e.what());
      }
    }
    // Returns the height collection of rendered pages.
    else if (method_call.method_name().compare("getPagesHeight") == 0)
    {
      // logger("getPagesHeight:");
      try
      {
        auto currentDocumentID = std::get<std::string>(*method_call.arguments());
        std::vector<double> pagesHeight;
        auto doc = getDocument(currentDocumentID);
        int pageCount = doc->getPageCount();
        for (int i = 0; i < pageCount; i++)
        {
          std::shared_ptr<Page> page = openPage(currentDocumentID, i);
          auto details = page->getDetails();
          pagesHeight.push_back(static_cast<double>(details.height));
          // page.reset(); //free
          closePage(page->id);
        }
        result->Success(flutter::EncodableValue(pagesHeight));
      }
      catch (std::exception &e)
      {
        // logger(e.what());
        result->Error("SyncfusionFlutterPdfViewerPlugin_exception", e.what());
      }
    }

    /// Gets the pdf page image from the specified page
    else if (method_call.method_name().compare("getImage") == 0)
    {
      // logger("getImage:");
      //  result->Error("SyncfusionFlutterPdfViewerPlugin_exception", "getImage");
      //  return;
      auto *arguments = std::get_if<flutter::EncodableMap>(method_call.arguments());
      auto document_id_it = arguments->find(flutter::EncodableValue("documentID"));
      if (document_id_it == arguments->end())
      {
        result->Error("SyncfusionFlutterPdfViewerPlugin_exception", "documentId is required");
        return;
      }
      auto documentID = std::get<std::string>(document_id_it->second);

      auto vPageIndex = arguments->find(flutter::EncodableValue("index"));
      if (vPageIndex == arguments->end())
      {
        result->Error("SyncfusionFlutterPdfViewerPlugin_exception", "page index is required");
        return;
      }
      auto index = std::get<int>(vPageIndex->second) - 1;

      auto vScale = arguments->find(flutter::EncodableValue("scale"));
      if (vScale == arguments->end())
      {
        result->Error("SyncfusionFlutterPdfViewerPlugin_exception", "scale is required");
        return;
      }

      double scale = std::get<double>(vScale->second);
      if (scale < 2)
      {
        scale = 2;
      }
      try
      {
        std::shared_ptr<Page> page = openPage(documentID, index);
        auto details = page->getDetails();

        // bool crop = { false };
        CropDetails *cropDetails = nullptr;
        pdfx::ImageFormat format = PNG;
        // Get background color
        // std::string backgroundStr = "FFFFFFFF"; // as int 268435455
        // backgroundStr.erase(0, 1);
        // unsigned long background = std::stoul(backgroundStr, nullptr, 16);
        unsigned long background = 268435455;
        // logger(std::to_string(background));
        // static_cast<int>()
        int width = (int)std::round(details.width * scale);
        int height = (int)std::round(details.height * scale);

        // Render page
        auto render = page->render(width, height, format, background, cropDetails);

        closePage(page->id);

        result->Success(flutter::EncodableValue(render.data));
      }
      catch (std::exception &e)
      {
        result->Error("SyncfusionFlutterPdfViewerPlugin_exception", e.what());
      }
    }
    else
    {
      result->NotImplemented();
    }
  }

} // namespace pdfx

void SyncfusionFlutterPdfViewerPluginRegisterWithRegistrar(
    FlutterDesktopPluginRegistrarRef registrar)
{
  pdfx::SyncfusionFlutterPdfViewerPlugin::RegisterWithRegistrar(
      flutter::PluginRegistrarManager::GetInstance()
          ->GetRegistrar<flutter::PluginRegistrarWindows>(registrar));
}

pdfx.cpp

#pragma warning(disable : 4458)

// Windows imports
#include <Windows.h>
#include <gdiplus.h>

// for log
#include <fstream>
//#include <time.h>
#include <ctime>
#include <sstream>

// std imports
#include <iostream>
#include <stdexcept>
#include <string>
#include <unordered_map>

#include "pdfx.h"

#pragma comment(lib, "gdiplus.lib")

namespace pdfx
{
  int GetEncoderClsid(const WCHAR *format, CLSID *pClsid)
  {
    UINT num = 0;  // number of image encoders
    UINT size = 0; // size of the image encoder array in bytes

    Gdiplus::ImageCodecInfo *pImageCodecInfo = NULL;

    Gdiplus::GetImageEncodersSize(&num, &size);
    if (size == 0)
      return -1; // Failure

    pImageCodecInfo = (Gdiplus::ImageCodecInfo *)(malloc(size));
    if (pImageCodecInfo == NULL)
      return -1; // Failure

    GetImageEncoders(num, size, pImageCodecInfo);

    for (UINT j = 0; j < num; ++j)
    {
      if (wcscmp(pImageCodecInfo[j].MimeType, format) == 0)
      {
        *pClsid = pImageCodecInfo[j].Clsid;
        free(pImageCodecInfo);
        return j; // Success
      }
    }

    free(pImageCodecInfo);
    return -1; // Failure
  }

  std::unordered_map<std::string, std::shared_ptr<Document>> document_repository;
  std::unordered_map<std::string, std::shared_ptr<Page>> page_repository;
  int lastId = 0;

  std::shared_ptr<Document> getDocument(std::string docId)
  {
    auto doc = document_repository.find(docId);
    if (doc == document_repository.end())
    {
      throw std::invalid_argument("Document is not open");
    }
    return doc->second;
  }

  std::shared_ptr<Document> openDocument(std::vector<uint8_t> data, std::string docId)
  {
    if (document_repository.size() == 0)
    {
      FPDF_LIBRARY_CONFIG config;
      config.version = 2;
      config.m_pUserFontPaths = NULL;
      config.m_pIsolate = NULL;
      config.m_v8EmbedderSlot = 0;
      FPDF_InitLibraryWithConfig(&config);
    }

    // lastId++;
    // std::string strId = std::to_string(lastId);

    std::shared_ptr<Document> doc = std::make_shared<Document>(data, docId);
    document_repository[docId] = doc;

    return doc;
  }

  std::shared_ptr<Document> openDocument(std::string name, std::string docId)
  {
    if (document_repository.size() == 0)
    {
      FPDF_LIBRARY_CONFIG config;
      config.version = 2;
      config.m_pUserFontPaths = NULL;
      config.m_pIsolate = NULL;
      config.m_v8EmbedderSlot = 0;
      FPDF_InitLibraryWithConfig(&config);
    }

    // lastId++;
    // std::string strId = std::to_string(lastId);

    std::shared_ptr<Document> doc = std::make_shared<Document>(name, docId);
    document_repository[docId] = doc;

    return doc;
  }

  void closeDocument(std::string strId)
  {
    document_repository.erase(strId);

    if (document_repository.size() == 0)
    {
      FPDF_DestroyLibrary();
    }
  }

  std::shared_ptr<Page> openPage(std::string docId, int index)
  {
    // lastId++;
    std::string indexStr = std::to_string(index);
    auto doc = document_repository.find(docId);
    if (doc == document_repository.end())
    {
      throw std::invalid_argument("Document is not open");
    }
    std::shared_ptr<Page> page =
        std::make_shared<Page>(doc->second, index, indexStr);
    page_repository[indexStr] = page;
    return page;
  }

  void closePage(std::string id) { page_repository.erase(id); }

  PageRender renderPage(std::string id, int width, int height, ImageFormat format,
                        std::string backgroundStr, CropDetails *crop)
  {
    auto page = page_repository.find(id);
    if (page == page_repository.end())
    {
      throw std::invalid_argument("Page does not exist");
    }

    // Get background color
    backgroundStr.erase(0, 1);
    auto background = std::stoul(backgroundStr, nullptr, 16);

    // Render page
    return page->second->render(width, height, format, background, crop);
  }

  //

  Document::Document(std::vector<uint8_t> dataRef, std::string id) : id{id}
  {
    // Copy data into object to keep it in memory
    data.swap(dataRef);

    document = FPDF_LoadMemDocument64(data.data(), data.size(), nullptr);
    if (!document)
    {
      throw std::invalid_argument("Document failed to open");
    }
  }

  Document::Document(std::string file, std::string id) : id{id}
  {
    document = FPDF_LoadDocument(file.c_str(), nullptr);
    if (!document)
    {
      throw std::invalid_argument("Document failed to open");
    }
  }

  Document::~Document() { FPDF_CloseDocument(document); }

  int Document::getPageCount() { return FPDF_GetPageCount(document); }

  Page::Page(std::shared_ptr<Document> doc, int index, std::string id) : id(id)
  {
    page = FPDF_LoadPage(doc->document, index);
    if (!page)
    {
      throw std::invalid_argument("Page failed to open");
    }
  }

  Page::~Page() { FPDF_ClosePage(page); }

  PageDetails Page::getDetails()
  {
    int width = static_cast<int>(FPDF_GetPageWidthF(page) + 0.5f);
    int height = static_cast<int>(FPDF_GetPageHeightF(page) + 0.5f);

    return PageDetails(width, height);
  }

  PageRender Page::render(int width, int height, ImageFormat format,
                          unsigned long background, CropDetails *crop)
  {
    int rWidth, rHeight, start_x, size_x, start_y, size_y;
    if (crop == nullptr)
    {
      rWidth = width;
      rHeight = height;
      start_x = 0;
      size_x = width;
      start_y = 0;
      size_y = height;
    }
    else
    {
      rWidth = crop->crop_width;
      rHeight = crop->crop_height;

      start_x = 0 - crop->crop_x;
      size_x = width;
      start_y = 0 - crop->crop_y;
      size_y = height;
    }

    // Create empty bitmap and render page onto it
    auto bitmap = FPDFBitmap_Create(rWidth, rHeight, 0);
    FPDFBitmap_FillRect(bitmap, 0, 0, rWidth, rHeight, background);
    FPDF_RenderPageBitmap(bitmap, page, start_x, start_y, size_x, size_y, 0,
                          FPDF_ANNOT | FPDF_LCD_TEXT);

    // Convert bitmap into RGBA format
    uint8_t *p = static_cast<uint8_t *>(FPDFBitmap_GetBuffer(bitmap));
    auto stride = FPDFBitmap_GetStride(bitmap);

    // Convert to image format
    Gdiplus::GdiplusStartupInput gdiplusStartupInput;
    ULONG_PTR gdiplusToken;
    Gdiplus::GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);

    // Get the CLSID of the image encoder.
    CLSID encoderClsid;
    switch (format)
    {
    case PNG:
      GetEncoderClsid(L"image/png", &encoderClsid);
      break;
    case JPEG:
      GetEncoderClsid(L"image/jpeg", &encoderClsid);
      break;
    }

    // Create gdi+ bitmap from raw image data
    auto winBitmap =
        new Gdiplus::Bitmap(rWidth, rHeight, stride, PixelFormat32bppRGB, p);

    // Create stream for converted image
    IStream *istream = nullptr;
    CreateStreamOnHGlobal(NULL, TRUE, &istream);

    // Encode image onto stream
    auto stat = winBitmap->Save(istream, &encoderClsid, NULL);
    if (stat == Gdiplus::OutOfMemory)
    {
      throw std::exception("Failed to encode to image, out of memory");
    }
    else if (stat != Gdiplus::Ok)
    {
      throw std::exception("Failed to encode to image");
    }

    // Get raw memory of stream
    HGLOBAL hg = NULL;
    GetHGlobalFromStream(istream, &hg);

    // copy IStream to buffer
    size_t bufsize = GlobalSize(hg);
    std::vector<uint8_t> data;
    data.resize(bufsize);

    // lock & unlock memory
    LPVOID pimage = GlobalLock(hg);
    memcpy(&data[0], pimage, bufsize);
    GlobalUnlock(hg);

    // Close stream
    istream->Release();

    // Cleanup gid+
    delete winBitmap;
    Gdiplus::GdiplusShutdown(gdiplusToken);

    FPDFBitmap_Destroy(bitmap);

    return PageRender(data, rWidth, rHeight);
  }

  std::string getCurrentDateTime(std::string s)
  {
    time_t now = time(0);
    struct tm timeinfo;
    localtime_s(&timeinfo ,&now);

    char buffer[80] = { 0 };

    if (s == "now")
    {
      strftime(buffer, sizeof(buffer), "%Y-%m-%d %X", &timeinfo);
    }
    else if (s == "date")
    {
      strftime(buffer, sizeof(buffer), "%Y-%m-%d", &timeinfo);
    }
    std::string str(buffer);

    return str;
  };

  void logger(std::string logMsg)
  {

    std::string filePath = "C:/MyDartProjects/flutter_pdf/flutter_pdfviewer/example/logs.txt";
    std::string now = getCurrentDateTime("now");
    std::ofstream ofs(filePath.c_str(), std::ios_base::out | std::ios_base::app);
    ofs << now << '\t' << logMsg << '\n';
    ofs.close();
  }

} // namespace pdfx

pdfx.h

#ifndef pdfx_H_
#define pdfx_H_

#include <fpdfview.h>

#include <memory>
#include <string>
#include <vector>

namespace pdfx
{

  enum ImageFormat
  {
    JPEG = 0,
    PNG = 1,
  };

  struct CropDetails
  {
    int crop_x;
    int crop_y;
    int crop_height;
    int crop_width;
  };

  struct PageDetails
  {
    const int width;
    const int height;

    PageDetails(int width, int height) : width(width), height(height) {}
  };

  struct PageRender
  {
    const std::vector<uint8_t> data;
    const int width;
    const int height;

    PageRender(std::vector<uint8_t> data, int width, int height)
        : data(data), width(width), height(height) {}
  };

  class Document
  {
  private:
    std::vector<uint8_t> data;

  public:
    Document(std::vector<uint8_t> data, std::string id);
    Document(std::string file, std::string id);

    ~Document();

    std::string id;
    FPDF_DOCUMENT document;

    int getPageCount(void);
  };

  class Page
  {
  private:
    FPDF_PAGE page;

  public:
    Page(std::shared_ptr<Document> doc, int index, std::string id);
    ~Page();

    std::string id;

    PageDetails getDetails();
    PageRender render(int width, int height, ImageFormat format,
                      unsigned long background, CropDetails *crop);
  };
  std::shared_ptr<Document> getDocument(std::string docId);
  std::shared_ptr<Document> openDocument(std::vector<uint8_t> data, std::string docId);
  std::shared_ptr<Document> openDocument(std::string name, std::string docId);

  void closeDocument(std::string id);
  std::shared_ptr<Page> openPage(std::string docId, int index);
  void closePage(std::string id);
  PageRender renderPage(std::string id, int width, int height, ImageFormat format,
                        std::string backgroundStr, CropDetails *crop);

  std::string getCurrentDateTime(std::string s);
  void logger(std::string logMsg);

} // namespace pdfx

#endif // pdfx_H_

CMakeLists.txt

cmake_minimum_required(VERSION 3.15)
set(PROJECT_NAME "syncfusion_flutter_pdfviewer_plugin")
project(${PROJECT_NAME} LANGUAGES CXX)

set(ARCH "x64")
set(PDFIUM_VERSION "4634" CACHE STRING "Version of pdfium used")

string(COMPARE GREATER_EQUAL ${PDFIUM_VERSION} "4690" PDFIUM_BINARY_NEW_FORMAT)

if(${PDFIUM_VERSION} STREQUAL "latest")
  set(PDFIUM_URL "https://github.com/bblanchon/pdfium-binaries/releases/latest/download/pdfium-windows-${ARCH}.zip")
elseif(PDFIUM_BINARY_NEW_FORMAT)
  set(PDFIUM_URL "https://github.com/bblanchon/pdfium-binaries/releases/download/chromium/${PDFIUM_VERSION}/pdfium-win-${ARCH}.tgz")
else()
  set(PDFIUM_URL "https://github.com/bblanchon/pdfium-binaries/releases/download/chromium/${PDFIUM_VERSION}/pdfium-windows-${ARCH}.zip")
endif()

# Download pdfium
include(../windows/DownloadProject.cmake)
download_project(
  PROJ
  pdfium
  URL
  ${PDFIUM_URL}
)

# This value is used when generating builds using this plugin, so it must
# not be changed
set(PLUGIN_NAME "syncfusion_flutter_pdfviewer_plugin")

include(${pdfium_SOURCE_DIR}/PDFiumConfig.cmake)

add_library(${PLUGIN_NAME} SHARED
  "pdfx.cpp"
  "pdfx.h"
  "syncfusion_flutter_pdf_viewer_plugin.cpp"
)
#isaque comentou
apply_standard_settings(${PLUGIN_NAME})

set_target_properties(${PLUGIN_NAME} PROPERTIES
  CXX_VISIBILITY_PRESET hidden)
target_compile_definitions(${PLUGIN_NAME} PRIVATE FLUTTER_PLUGIN_IMPL)
target_include_directories(${PLUGIN_NAME} INTERFACE
  "${CMAKE_CURRENT_SOURCE_DIR}/include")
target_link_libraries(${PLUGIN_NAME} PRIVATE pdfium flutter flutter_wrapper_plugin)

# List of absolute paths to libraries that should be bundled with the plugin
set(pdfx_bundled_libraries
  "${PDFium_LIBRARY}"
  PARENT_SCOPE
)
dillibabu1802 commented 2 years ago

Hi @insinfo

Thank you for your valuable suggestions and thoughts.

We have already implemented Windows support for Flutter PDFViewer and it is in the testing phase. The feature is expected to be available in our Essential Studio 2022 Volume 1 main release which is estimated to be released by the end of March 2022 tentatively. So, we request you to wait till the release to be rolled out. We will let you know once the feature is integrated.

Regards, Dilli babu