From 2689037bde1646a4cedafbba968d187ae24f3d14 Mon Sep 17 00:00:00 2001
From: Alex Shvartzkop <dudesuchamazing@gmail.com>
Date: Fri, 7 Jul 2023 19:43:41 +0300
Subject: [PATCH] PDF plotting: support bitmaps with transparency.

Fixes https://gitlab.com/kicad/code/kicad/-/issues/5979
---
 common/plotters/PDF_plotter.cpp           | 308 +++++++++++++++++-----
 common/plotters/common_plot_functions.cpp |  34 ++-
 include/plotters/plotters_pslike.h        |   3 +
 3 files changed, 261 insertions(+), 84 deletions(-)

diff --git a/common/plotters/PDF_plotter.cpp b/common/plotters/PDF_plotter.cpp
index 23dcc1e7ab..b221b24404 100644
--- a/common/plotters/PDF_plotter.cpp
+++ b/common/plotters/PDF_plotter.cpp
@@ -33,6 +33,8 @@
 #include <wx/filename.h>
 #include <wx/mstream.h>
 #include <wx/zstream.h>
+#include <wx/wfstream.h>
+#include <wx/datstrm.h>
 
 #include <advanced_config.h>
 #include <eda_text.h> // for IsGotoPageHref
@@ -471,6 +473,53 @@ void PDF_PLOTTER::PlotImage( const wxImage& aImage, const VECTOR2I& aPos, double
     VECTOR2I start( aPos.x - drawsize.x / 2, aPos.y + drawsize.y / 2 );
     VECTOR2D dev_start = userToDeviceCoordinates( start );
 
+    // Deduplicate images
+    auto findHandleForImage = [&]( const wxImage& aImage ) -> int
+    {
+        for( const auto& [imgHandle, image] : m_imageHandles )
+        {
+            if( image.IsSameAs( aImage ) )
+                return imgHandle;
+
+            if( image.GetWidth() != aImage.GetWidth() )
+                continue;
+
+            if( image.GetHeight() != aImage.GetHeight() )
+                continue;
+
+            if( image.GetType() != aImage.GetType() )
+                continue;
+
+            if( image.HasAlpha() != aImage.HasAlpha() )
+                continue;
+
+            if( image.HasMask() != aImage.HasMask() || image.GetMaskRed() != aImage.GetMaskRed()
+                || image.GetMaskGreen() != aImage.GetMaskGreen()
+                || image.GetMaskBlue() != aImage.GetMaskBlue() )
+                continue;
+
+            int pixCount = image.GetWidth() * image.GetHeight();
+            
+            if( memcmp( image.GetData(), aImage.GetData(), pixCount * 3 ) != 0 )
+                continue;
+
+            if( image.HasAlpha() && memcmp( image.GetAlpha(), aImage.GetAlpha(), pixCount ) != 0 )
+                continue;
+
+            return imgHandle;
+        }
+
+        return -1;
+    };
+
+    int imgHandle = findHandleForImage( aImage );
+
+    if( imgHandle == -1 )
+    {
+        imgHandle = allocPdfObject();
+        m_imageHandles.emplace( imgHandle, aImage );
+    }
+
     /* PDF has an uhm... simplified coordinate system handling. There is
        *one* operator to do everything (the PS concat equivalent). At least
        they kept the matrix stack to save restore environments. Also images
@@ -486,75 +535,8 @@ void PDF_PLOTTER::PlotImage( const wxImage& aImage, const VECTOR2I& aPos, double
              userToDeviceSize( drawsize.y ),
              dev_start.x, dev_start.y );
 
-    /* An inline image is a cross between a dictionary and a stream.
-       A real ugly construct (compared with the elegance of the PDF
-       format). Also it accepts some 'abbreviations', which is stupid
-       since the content stream is usually compressed anyway... */
-    fprintf( m_workFile,
-             "BI\n"
-             "  /BPC 8\n"
-             "  /CS %s\n"
-             "  /W %d\n"
-             "  /H %d\n"
-             "ID\n", m_colorMode ? "/RGB" : "/G", pix_size.x, pix_size.y );
-
-    wxColor bg = m_renderSettings->GetBackgroundColor() != COLOR4D::UNSPECIFIED
-                         ? m_renderSettings->GetBackgroundColor().ToColour()
-                         : wxColor( 255, 255, 255 );
-
-    /* Here comes the stream (in binary!). I *could* have hex or ascii84
-       encoded it, but who cares? I'll go through zlib anyway */
-    for( int y = 0; y < pix_size.y; y++ )
-    {
-        for( int x = 0; x < pix_size.x; x++ )
-        {
-            unsigned char r = aImage.GetRed( x, y ) & 0xFF;
-            unsigned char g = aImage.GetGreen( x, y ) & 0xFF;
-            unsigned char b = aImage.GetBlue( x, y ) & 0xFF;
-
-            // PDF inline images don't support alpha, so blend with background color
-            if( aImage.HasAlpha() )
-            {
-                unsigned char alpha = aImage.GetAlpha( x, y ) & 0xFF;
-
-                if( alpha < 0xFF )
-                {
-                    float d = alpha / 255.0;
-
-                    r = wxColour::AlphaBlend( r, bg.Red(), d );
-                    g = wxColour::AlphaBlend( g, bg.Green(), d );
-                    b = wxColour::AlphaBlend( b, bg.Blue(), d );
-                }
-            }
-
-            if( aImage.HasMask() )
-            {
-                if( r == aImage.GetMaskRed() && g == aImage.GetMaskGreen()
-                  && b == aImage.GetMaskBlue() )
-                {
-                    r = 0xFF;
-                    g = 0xFF;
-                    b = 0xFF;
-                }
-            }
-
-            // As usual these days, stdio buffering has to suffeeeeerrrr
-            if( m_colorMode )
-            {
-                putc( r, m_workFile );
-                putc( g, m_workFile );
-                putc( b, m_workFile );
-            }
-            else
-            {
-                // Greyscale conversion (CIE 1931)
-                unsigned char grey = KiROUND( r * 0.2126 + g * 0.7152 + b * 0.0722 );
-                putc( grey, m_workFile );
-            }
-        }
-    }
-
-    fputs( "EI Q\n", m_workFile ); // Finish step 2 and do step 3
+    fprintf( m_workFile, "/Im%d Do\n", imgHandle );
+    fputs( "Q\n", m_workFile );
 }
 
 
@@ -712,6 +694,82 @@ void PDF_PLOTTER::StartPage( const wxString& aPageNumber, const wxString& aPageN
 }
 
 
+void WriteImageStream( const wxImage& aImage, wxDataOutputStream& aOut, wxColor bg, bool colorMode )
+{
+    int w = aImage.GetWidth();
+    int h = aImage.GetHeight();
+
+    for( int y = 0; y < h; y++ )
+    {
+        for( int x = 0; x < w; x++ )
+        {
+            unsigned char r = aImage.GetRed( x, y ) & 0xFF;
+            unsigned char g = aImage.GetGreen( x, y ) & 0xFF;
+            unsigned char b = aImage.GetBlue( x, y ) & 0xFF;
+
+            if( aImage.HasMask() )
+            {
+                if( r == aImage.GetMaskRed() && g == aImage.GetMaskGreen()
+                    && b == aImage.GetMaskBlue() )
+                {
+                    r = bg.Red();
+                    g = bg.Green();
+                    b = bg.Blue();
+                }
+            }
+
+            if( colorMode )
+            {
+                aOut.Write8( r );
+                aOut.Write8( g );
+                aOut.Write8( b );
+            }
+            else
+            {
+                // Greyscale conversion (CIE 1931)
+                unsigned char grey = KiROUND( r * 0.2126 + g * 0.7152 + b * 0.0722 );
+                
+                aOut.Write8( grey );
+            }
+        }
+    }
+}
+
+
+void WriteImageSMaskStream( const wxImage& aImage, wxDataOutputStream& aOut )
+{
+    int w = aImage.GetWidth();
+    int h = aImage.GetHeight();
+
+    if( aImage.HasMask() )
+    {
+        for( int y = 0; y < h; y++ )
+        {
+            for( int x = 0; x < w; x++ )
+            {
+                unsigned char a = 255;
+                unsigned char r = aImage.GetRed( x, y );
+                unsigned char g = aImage.GetGreen( x, y );
+                unsigned char b = aImage.GetBlue( x, y );
+
+                if( r == aImage.GetMaskRed() && g == aImage.GetMaskGreen()
+                    && b == aImage.GetMaskBlue() )
+                {
+                    a = 0;
+                }
+
+                aOut.Write8( a );
+            }
+        }
+    }
+    else if( aImage.HasAlpha() )
+    {
+        int size = w * h;
+        aOut.Write8( aImage.GetAlpha(), size );
+    }
+}
+
+
 void PDF_PLOTTER::ClosePage()
 {
     wxASSERT( m_workFile );
@@ -807,11 +865,13 @@ void PDF_PLOTTER::ClosePage()
              "/Parent %d 0 R\n"
              "/Resources <<\n"
              "    /ProcSet [/PDF /Text /ImageC /ImageB]\n"
-             "    /Font %d 0 R >>\n"
+             "    /Font %d 0 R\n"
+             "    /XObject %d 0 R >>\n"
              "/MediaBox [0 0 %g %g]\n"
              "/Contents %d 0 R\n",
              m_pageTreeHandle,
              m_fontResDictHandle,
+             m_imgResDictHandle,
              psPaperSize.x,
              psPaperSize.y,
              m_pageStreamHandle );
@@ -909,6 +969,8 @@ bool PDF_PLOTTER::StartPlot( const wxString& aPageNumber, const wxString& aPageN
        (it *could* be inherited via the Pages tree */
     m_fontResDictHandle = allocPdfObject();
 
+    m_imgResDictHandle = allocPdfObject();
+
     m_jsNamesHandle = allocPdfObject();
 
     /* Now, the PDF is read from the end, (more or less)... so we start
@@ -1109,6 +1171,112 @@ bool PDF_PLOTTER::EndPlot()
     fputs( ">>\n", m_outputFile );
     closePdfObject();
 
+    // Named image dictionary (was allocated, now we emit it)
+    startPdfObject( m_imgResDictHandle );
+    fputs( "<<\n", m_outputFile );
+
+    for( const auto& [imgHandle, image] : m_imageHandles )
+    {
+        fprintf( m_outputFile, "    /Im%d %d 0 R\n", imgHandle, imgHandle );
+    }
+
+    fputs( ">>\n", m_outputFile );
+    closePdfObject();
+
+    // Emit images with optional SMask for transparency
+    for( const auto& [imgHandle, image] : m_imageHandles )
+    {
+        // Init wxFFile so wxFFileOutputStream won't close file in dtor.
+        wxFFile outputFFile( m_outputFile );
+
+        // Image
+        startPdfObject( imgHandle );
+        int imgLenHandle = allocPdfObject();
+        int smaskHandle = ( image.HasAlpha() || image.HasMask() ) ? allocPdfObject() : -1;
+
+        fprintf( m_outputFile,
+                 "<<\n"
+                 "/Type /XObject\n"
+                 "/Subtype /Image\n"
+                 "/BitsPerComponent 8\n"
+                 "/ColorSpace %s\n"
+                 "/Width %d\n"
+                 "/Height %d\n"
+                 "/Filter /FlateDecode\n"
+                 "/Length %d 0 R\n", // Length is deferred
+                 m_colorMode ? "/DeviceRGB" : "/DeviceGray", image.GetWidth(), image.GetHeight(),
+                 imgLenHandle );
+
+        if( smaskHandle != -1 )
+            fprintf( m_outputFile, "/SMask %d 0 R\n", smaskHandle );
+
+        fputs( ">>\n", m_outputFile );
+        fputs( "stream\n", m_outputFile );
+
+        long imgStreamStart = ftell( m_outputFile );
+
+        {
+            wxFFileOutputStream ffos( outputFFile );
+            wxZlibOutputStream  zos( ffos, wxZ_BEST_COMPRESSION, wxZLIB_ZLIB );
+            wxDataOutputStream  dos( zos );
+
+            WriteImageStream( image, dos, m_renderSettings->GetBackgroundColor().ToColour(),
+                              m_colorMode );
+        }
+
+        long imgStreamSize = ftell( m_outputFile ) - imgStreamStart;
+
+        fputs( "\nendstream\n", m_outputFile );
+        closePdfObject();
+
+        startPdfObject( imgLenHandle );
+        fprintf( m_outputFile, "%ld\n", imgStreamSize );
+        closePdfObject();
+
+        if( smaskHandle != -1 )
+        {
+            // SMask
+            startPdfObject( smaskHandle );
+            int smaskLenHandle = allocPdfObject();
+
+            fprintf( m_outputFile,
+                     "<<\n"
+                     "/Type /XObject\n"
+                     "/Subtype /Image\n"
+                     "/BitsPerComponent 8\n"
+                     "/ColorSpace /DeviceGray\n"
+                     "/Width %d\n"
+                     "/Height %d\n"
+                     "/Length %d 0 R\n"
+                     "/Filter /FlateDecode\n"
+                     ">>\n", // Length is deferred
+                     image.GetWidth(), image.GetHeight(), smaskLenHandle );
+
+            fputs( "stream\n", m_outputFile );
+
+            long smaskStreamStart = ftell( m_outputFile );
+
+            {
+                wxFFileOutputStream ffos( outputFFile );
+                wxZlibOutputStream  zos( ffos, wxZ_BEST_COMPRESSION, wxZLIB_ZLIB );
+                wxDataOutputStream  dos( zos );
+
+                WriteImageSMaskStream( image, dos );
+            }
+
+            long smaskStreamSize = ftell( m_outputFile ) - smaskStreamStart;
+
+            fputs( "\nendstream\n", m_outputFile );
+            closePdfObject();
+
+            startPdfObject( smaskLenHandle );
+            fprintf( m_outputFile, "%u\n", (unsigned) smaskStreamSize );
+            closePdfObject();
+        }
+
+        outputFFile.Detach(); // Don't close it
+    }
+
     for( const auto& [ linkHandle, linkPair ] : m_hyperlinkHandles )
     {
         const BOX2D&    box = linkPair.first;
diff --git a/common/plotters/common_plot_functions.cpp b/common/plotters/common_plot_functions.cpp
index e7a902d287..4b296e8160 100644
--- a/common/plotters/common_plot_functions.cpp
+++ b/common/plotters/common_plot_functions.cpp
@@ -85,9 +85,28 @@ void PlotDrawingSheet( PLOTTER* plotter, const PROJECT* aProject, const TITLE_BL
 
     drawList.BuildDrawItemsList( aPageInfo, aTitleBlock );
 
-    // Draw item list
+    // Draw bitmaps first
     for( DS_DRAW_ITEM_BASE* item = drawList.GetFirst(); item; item = drawList.GetNext() )
     {
+        if( item->Type() == WSG_BITMAP_T )
+        {
+            DS_DRAW_ITEM_BITMAP* drawItem = (DS_DRAW_ITEM_BITMAP*) item;
+            DS_DATA_ITEM_BITMAP* bitmap = (DS_DATA_ITEM_BITMAP*) drawItem->GetPeer();
+
+            if( bitmap->m_ImageBitmap == nullptr )
+                continue;
+
+            bitmap->m_ImageBitmap->PlotImage( plotter, drawItem->GetPosition(), plotColor,
+                                              PLOTTER::USE_DEFAULT_LINE_WIDTH );
+        }
+    }
+
+    // Draw other items
+    for( DS_DRAW_ITEM_BASE* item = drawList.GetFirst(); item; item = drawList.GetNext() )
+    {
+        if( item->Type() == WSG_BITMAP_T )
+            continue;
+
         plotter->SetColor( plotColor );
         plotter->SetCurrentLineWidth( PLOTTER::USE_DEFAULT_LINE_WIDTH );
 
@@ -157,19 +176,6 @@ void PlotDrawingSheet( PLOTTER* plotter, const PROJECT* aProject, const TITLE_BL
         }
             break;
 
-        case WSG_BITMAP_T:
-        {
-            DS_DRAW_ITEM_BITMAP* drawItem = (DS_DRAW_ITEM_BITMAP*) item;
-            DS_DATA_ITEM_BITMAP* bitmap = (DS_DATA_ITEM_BITMAP*) drawItem->GetPeer();
-
-            if( bitmap->m_ImageBitmap == nullptr )
-                break;
-
-            bitmap->m_ImageBitmap->PlotImage( plotter, drawItem->GetPosition(), plotColor,
-                                              PLOTTER::USE_DEFAULT_LINE_WIDTH );
-        }
-            break;
-
         default:
             wxFAIL_MSG( "PlotDrawingSheet(): Unknown drawing sheet item." );
             break;
diff --git a/include/plotters/plotters_pslike.h b/include/plotters/plotters_pslike.h
index 8a522a68d3..76b777181e 100644
--- a/include/plotters/plotters_pslike.h
+++ b/include/plotters/plotters_pslike.h
@@ -493,6 +493,7 @@ protected:
 
     int m_pageTreeHandle;           ///< Handle to the root of the page tree object
     int m_fontResDictHandle;        ///< Font resource dictionary
+    int m_imgResDictHandle;         ///< Image resource dictionary
     int m_jsNamesHandle;            ///< Handle for Names dictionary with JS
     std::vector<int> m_pageHandles; ///< Handles to the page objects
     int m_pageStreamHandle;         ///< Handle of the page content object
@@ -515,6 +516,8 @@ protected:
 
     std::map<wxString, std::vector<std::pair<BOX2I, wxString>>>      m_bookmarksInPage;
 
+    std::map<int, wxImage> m_imageHandles;
+
     std::unique_ptr<OUTLINE_NODE> m_outlineRoot;    ///< Root outline node
     int                           m_totalOutlineNodes;  ///< Total number of outline nodes
 };