Skip to content

Commit 8dba2b7

Browse files
vvolkllinev
authored andcommitted
Implement TASImage::Paint() for PDF
1 parent 13c3fdc commit 8dba2b7

7 files changed

Lines changed: 514 additions & 176 deletions

File tree

graf2d/asimage/src/TASImage.cxx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1595,9 +1595,6 @@ void TASImage::Paint(Option_t *option)
15951595
min, max, ndiv, "+L");
15961596
}
15971597
return;
1598-
} else if (ps->InheritsFrom("TPDF")) {
1599-
Warning("Paint", "PDF not implemented yet");
1600-
return;
16011598
} else if (ps->InheritsFrom("TSVG")) {
16021599
paint_as_png = kTRUE;
16031600
}

graf2d/gpad/test/CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,7 @@
77
ROOT_ADD_GTEST(TRatioPlot ratioplot.cxx LIBRARIES Gpad)
88
ROOT_ADD_GTEST(TPad pdftitle.cxx LIBRARIES Gpad)
99
ROOT_ADD_GTEST(TPDF pdfurl.cxx LIBRARIES Gpad)
10+
11+
if(asimage)
12+
ROOT_ADD_GTEST(TASImagePDF tasimage_pdf.cxx LIBRARIES Gpad ASImage ZLIB::ZLIB)
13+
endif()

graf2d/gpad/test/tasimage_pdf.cxx

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
#include "gtest/gtest.h"
2+
3+
#include "TCanvas.h"
4+
#include "TImage.h"
5+
#include "TString.h"
6+
#include "TSystem.h"
7+
8+
#include <fstream>
9+
#include <string>
10+
#include <vector>
11+
#include <zlib.h>
12+
13+
namespace {
14+
15+
// Read a binary file fully into a string.
16+
std::string SlurpFile(const TString &path)
17+
{
18+
std::ifstream in(path.Data(), std::ios::binary);
19+
std::string data((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());
20+
return data;
21+
}
22+
23+
// FlateDecode the byte range [begin, end) of `pdf`. Returns an empty string if
24+
// the range is not a valid zlib stream.
25+
std::string Inflate(const std::string &pdf, std::size_t begin, std::size_t end)
26+
{
27+
std::string out;
28+
if (end <= begin)
29+
return out;
30+
z_stream zs{};
31+
zs.next_in = reinterpret_cast<Bytef *>(const_cast<char *>(pdf.data() + begin));
32+
zs.avail_in = static_cast<uInt>(end - begin);
33+
if (inflateInit(&zs) != Z_OK)
34+
return out;
35+
std::vector<char> buf(64 * 1024);
36+
int rc = Z_OK;
37+
do {
38+
zs.next_out = reinterpret_cast<Bytef *>(buf.data());
39+
zs.avail_out = static_cast<uInt>(buf.size());
40+
rc = inflate(&zs, Z_NO_FLUSH);
41+
if (rc == Z_OK || rc == Z_STREAM_END)
42+
out.append(buf.data(), buf.size() - zs.avail_out);
43+
else
44+
break;
45+
} while (rc != Z_STREAM_END);
46+
inflateEnd(&zs);
47+
return out;
48+
}
49+
50+
// Locate the data range of the "stream ... endstream" block that starts at or
51+
// after `from`. Returns false if none is found.
52+
bool FindStream(const std::string &pdf, std::size_t from, std::size_t &dataBegin, std::size_t &dataEnd,
53+
std::size_t &next)
54+
{
55+
std::size_t s = pdf.find("stream", from);
56+
if (s == std::string::npos)
57+
return false;
58+
dataBegin = s + 6; // strlen("stream")
59+
if (dataBegin < pdf.size() && pdf[dataBegin] == '\r')
60+
++dataBegin;
61+
if (dataBegin < pdf.size() && pdf[dataBegin] == '\n')
62+
++dataBegin;
63+
std::size_t e = pdf.find("endstream", dataBegin);
64+
if (e == std::string::npos)
65+
return false;
66+
dataEnd = e;
67+
if (dataEnd > dataBegin && pdf[dataEnd - 1] == '\n')
68+
--dataEnd;
69+
if (dataEnd > dataBegin && pdf[dataEnd - 1] == '\r')
70+
--dataEnd;
71+
next = e + 9; // strlen("endstream")
72+
return true;
73+
}
74+
75+
// FlateDecode every stream in the PDF and concatenate the results. The page
76+
// content stream (which carries the painting operators) is among them.
77+
std::string DecodeAllFlateStreams(const std::string &pdf)
78+
{
79+
std::string out;
80+
std::size_t pos = 0, b, e, next;
81+
while (FindStream(pdf, pos, b, e, next)) {
82+
out += Inflate(pdf, b, e);
83+
pos = next;
84+
}
85+
return out;
86+
}
87+
88+
// FlateDecode the stream of the first image XObject in the PDF.
89+
std::string DecodeImageXObject(const std::string &pdf)
90+
{
91+
std::size_t img = pdf.find("/Subtype /Image");
92+
if (img == std::string::npos)
93+
return {};
94+
std::size_t b, e, next;
95+
if (!FindStream(pdf, img, b, e, next))
96+
return {};
97+
return Inflate(pdf, b, e);
98+
}
99+
100+
} // namespace
101+
102+
// Render a small synthetic TImage to a PDF and verify it is embedded as a
103+
// proper PDF image XObject: a "/Subtype /Image" object carrying the pixels,
104+
// referenced from the page content stream by a "/ImN Do" operator. The
105+
// embedded pixels are decoded back and checked against the four colours that
106+
// were drawn, so the test guards pixel fidelity, not just the PDF structure.
107+
//
108+
// This covers the TASImage::Paint -> TPDF::CellArray* path: before the fix it
109+
// emitted only a "PDF not implemented yet" warning and no image data.
110+
TEST(TASImage, PDFEmbedsImageXObject)
111+
{
112+
const TString pdfFile = "tasimage_pdf_embed.pdf";
113+
114+
TImage *img = TImage::Create();
115+
ASSERT_NE(img, nullptr) << "TImage::Create failed (ASImage plugin missing?)";
116+
// The first FillRectangle sizes the (initially empty) image, so it must
117+
// cover the whole 80x80 area; the other three then paint the quadrants.
118+
img->FillRectangle("#ff0000", 0, 0, 80, 80); // red base / top-left
119+
img->FillRectangle("#00ff00", 40, 0, 40, 40); // top-right green
120+
img->FillRectangle("#0000ff", 0, 40, 40, 40); // bottom-left blue
121+
img->FillRectangle("#ffff00", 40, 40, 40, 40); // bottom-right yellow
122+
123+
TCanvas c("tasimage_pdf_canvas", "tasimage_pdf_canvas", 300, 300);
124+
img->Draw("X");
125+
c.SaveAs(pdfFile);
126+
delete img;
127+
128+
FileStat_t st;
129+
ASSERT_EQ(gSystem->GetPathInfo(pdfFile, st), 0) << "PDF file was not created.";
130+
ASSERT_GT(st.fSize, 1024) << "PDF is suspiciously small.";
131+
132+
const std::string pdf = SlurpFile(pdfFile);
133+
ASSERT_FALSE(pdf.empty());
134+
ASSERT_EQ(pdf.compare(0, 4, "%PDF"), 0) << "File does not look like a PDF.";
135+
136+
// The bitmap must be stored as its own image XObject, not dropped.
137+
EXPECT_NE(pdf.find("/Subtype /Image"), std::string::npos)
138+
<< "No image XObject in the PDF — TASImage::Paint did not embed the bitmap.";
139+
EXPECT_NE(pdf.find("/ColorSpace /DeviceRGB"), std::string::npos)
140+
<< "Image XObject colour space declaration missing.";
141+
EXPECT_NE(pdf.find("/BitsPerComponent 8"), std::string::npos) << "Image XObject bit depth declaration missing.";
142+
143+
// The page content stream must actually paint that XObject.
144+
const std::string content = DecodeAllFlateStreams(pdf);
145+
ASSERT_FALSE(content.empty()) << "No Flate streams could be decoded.";
146+
EXPECT_NE(content.find("/Im1 Do"), std::string::npos)
147+
<< "Page content stream does not paint the image XObject (no '/Im1 Do').";
148+
149+
// The embedded pixels must match what was drawn: all four colours present.
150+
const std::string pixels = DecodeImageXObject(pdf);
151+
ASSERT_FALSE(pixels.empty()) << "Image XObject stream did not FlateDecode.";
152+
EXPECT_EQ(pixels.size() % 3u, 0u) << "DeviceRGB data is not a whole number of pixels.";
153+
EXPECT_NE(pixels.find(std::string("\xff\x00\x00", 3)), std::string::npos) << "red missing";
154+
EXPECT_NE(pixels.find(std::string("\x00\xff\x00", 3)), std::string::npos) << "green missing";
155+
EXPECT_NE(pixels.find(std::string("\x00\x00\xff", 3)), std::string::npos) << "blue missing";
156+
EXPECT_NE(pixels.find(std::string("\xff\xff\x00", 3)), std::string::npos) << "yellow missing";
157+
158+
gSystem->Unlink(pdfFile);
159+
}

graf2d/postscript/inc/TPDF.h

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,28 @@ class TPDF : public TVirtualPS {
6565
Double_t fE = 0.; ///< "e" value of the Current Transformation Matrix (CTM)
6666
Double_t fF = 0.; ///< "f" value of the Current Transformation Matrix (CTM)
6767

68+
// Transient scratch state for the in-flight Cell Array: only meaningful
69+
// between a CellArrayBegin and the matching CellArrayEnd, never streamed,
70+
// so every member below is marked transient (///<!).
71+
Int_t fCellArrayW = 0; ///<! Cell array width in cells
72+
Int_t fCellArrayH = 0; ///<! Cell array height in cells
73+
Double_t fCellArrayXpdf = 0.; ///<! PDF x of the image's left edge
74+
Double_t fCellArrayYpdfBot = 0.; ///<! PDF y of the image's bottom edge
75+
Double_t fCellArrayWpdf = 0.; ///<! PDF width of the image
76+
Double_t fCellArrayHpdf = 0.; ///<! PDF height of the image
77+
std::vector<unsigned char> fCellArrayRGB; ///<! Pixel buffer (3 bytes per pixel, top-to-bottom)
78+
79+
/// A bitmap embedded as a PDF image XObject. Buffered until Close() because
80+
/// a PDF object cannot be opened while a page content stream is still being
81+
/// written; the page stream only carries a placement matrix and a "/ImN Do".
82+
struct PDFImage {
83+
Int_t fW = 0; ///< Width in pixels
84+
Int_t fH = 0; ///< Height in pixels
85+
Bool_t fFlate = kTRUE; ///< True if fData is Flate-compressed
86+
std::vector<unsigned char> fData; ///< 8-bit DeviceRGB samples (raw or Flate)
87+
};
88+
std::vector<PDFImage> fImageObjects; ///<! Embedded image XObjects, flushed in Close()
89+
6890
static Int_t fgLineJoin; ///< Appearance of joining lines
6991
static Int_t fgLineCap; ///< Appearance of line caps
7092

0 commit comments

Comments
 (0)