KiCad PCB EDA Suite
Loading...
Searching...
No Matches
PDF_plotter.cpp
Go to the documentation of this file.
1/*
2 * This program source code file is part of KiCad, a free EDA CAD application.
3 *
4 * Copyright (C) 1992-2012 Lorenzo Marcantonio, [email protected]
5 * Copyright The KiCad Developers, see AUTHORS.txt for contributors.
6 *
7 * This program is free software; you can redistribute it and/or
8 * modify it under the terms of the GNU General Public License
9 * as published by the Free Software Foundation; either version 2
10 * of the License, or (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with this program; if not, you may find one here:
19 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
20 * or you may search the http://www.gnu.org website for the version 2 license,
21 * or you may write to the Free Software Foundation, Inc.,
22 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
23 */
24
25#include <algorithm>
26#include <iterator>
27#include <cstdio> // snprintf
28#include <stack>
29#include <ranges>
30
31#include <wx/filename.h>
32#include <wx/mstream.h>
33#include <wx/zstream.h>
34#include <wx/wfstream.h>
35#include <wx/datstrm.h>
36#include <wx/tokenzr.h>
37
38#include <advanced_config.h>
39#include <common.h> // ResolveUriByEnvVars
40#include <eda_text.h> // for IsGotoPageHref
41#include <font/font.h>
42#include <core/ignore.h>
43#include <macros.h>
44#include <trace_helpers.h>
45#include <trigo.h>
46#include <string_utils.h>
47#include <core/utf8.h>
48#include <markup_parser.h>
49#include <fmt/format.h>
50#include <fmt/chrono.h>
51#include <fmt/ranges.h>
52
56#include <geometry/shape_rect.h>
58
59
61
62#define GLM_ENABLE_EXPERIMENTAL //for older glm to enable euler angles
63#include <glm/glm.hpp>
64#include <glm/gtx/euler_angles.hpp>
65
66std::string PDF_PLOTTER::encodeStringForPlotter( const wxString& aText )
67{
68 // returns a string compatible with PDF string convention from a unicode string.
69 // if the initial text is only ASCII7, return the text between ( and ) for a good readability
70 // if the initial text is no ASCII7, return the text between < and >
71 // and encoded using 16 bits hexa (4 digits) by wide char (unicode 16)
72 std::string result;
73
74 // Is aText only ASCII7 ?
75 bool is_ascii7 = true;
76
77 for( size_t ii = 0; ii < aText.Len(); ii++ )
78 {
79 if( aText[ii] >= 0x7F )
80 {
81 is_ascii7 = false;
82 break;
83 }
84 }
85
86 if( is_ascii7 )
87 {
88 result = '(';
89
90 for( unsigned ii = 0; ii < aText.Len(); ii++ )
91 {
92 unsigned int code = aText[ii];
93
94 // These characters must be escaped
95 switch( code )
96 {
97 case '(':
98 case ')':
99 case '\\':
100 result += '\\';
102
103 default:
104 result += code;
105 break;
106 }
107 }
108
109 result += ')';
110 }
111 else
112 {
113 result = "<FEFF";
114
115 for( size_t ii = 0; ii < aText.Len(); ii++ )
116 {
117 unsigned int code = aText[ii];
118 result += fmt::format("{:04X}", code);
119 }
120
121 result += '>';
122 }
123
124 return result;
125}
126
127
128std::string PDF_PLOTTER::encodeDoubleForPlotter( double aValue ) const
129{
130 std::string buf = fmt::format( "{:g}", aValue );
131
132 // PDF syntax does not allow exponent notation (PostScript does). fmt's {:g} can emit it and
133 // can't be configured to force non-exponent output, so fall back to fixed when needed.
134 if( buf.find( 'e' ) != std::string::npos || buf.find( 'E' ) != std::string::npos )
135 buf = fmt::format( "{:.10f}", aValue );
136
137 if( buf.find( '.' ) != std::string::npos )
138 {
139 // Trim trailing zeros from fixed output while keeping at least one digit.
140 while( buf.size() > 1 && buf.back() == '0' )
141 buf.pop_back();
142
143 // Remove a dangling decimal point if we stripped all fractional digits.
144 if( !buf.empty() && buf.back() == '.' )
145 buf.pop_back();
146 }
147
148 // Avoid emitting "-0" for tiny negative values that round to zero.
149 if( buf == "-0" )
150 buf = "0";
151
152 return buf;
153}
154
155
156std::string PDF_PLOTTER::encodeByteString( const std::string& aBytes )
157{
158 std::string result;
159 result.reserve( aBytes.size() * 4 + 2 );
160 result.push_back( '(' );
161
162 for( unsigned char byte : aBytes )
163 {
164 if( byte == '(' || byte == ')' || byte == '\\' )
165 {
166 result.push_back( '\\' );
167 result.push_back( static_cast<char>( byte ) );
168 }
169 else if( byte < 32 || byte > 126 )
170 {
171 fmt::format_to( std::back_inserter( result ), "\\{:03o}", byte );
172 }
173 else
174 {
175 result.push_back( static_cast<char>( byte ) );
176 }
177 }
178
179 result.push_back( ')' );
180 return result;
181}
182
183
184bool PDF_PLOTTER::OpenFile( const wxString& aFullFilename )
185{
186 m_filename = aFullFilename;
187
188 wxASSERT( !m_outputFile );
189
190 // Open the PDF file in binary mode
191 m_outputFile = wxFopen( m_filename, wxT( "wb" ) );
192
193 if( m_outputFile == nullptr )
194 return false ;
195
196 return true;
197}
198
199
200void PDF_PLOTTER::SetViewport( const VECTOR2I& aOffset, double aIusPerDecimil,
201 double aScale, bool aMirror )
202{
203 m_plotMirror = aMirror;
204 m_plotOffset = aOffset;
205 m_plotScale = aScale;
206 m_IUsPerDecimil = aIusPerDecimil;
207
208 // The CTM is set to 1 user unit per decimal
209 m_iuPerDeviceUnit = 1.0 / aIusPerDecimil;
210
211 /* The paper size in this engine is handled page by page
212 Look in the StartPage function */
213}
214
215
216void PDF_PLOTTER::SetCurrentLineWidth( int aWidth, void* aData )
217{
218 wxASSERT( m_workFile );
219
220 if( aWidth == DO_NOT_SET_LINE_WIDTH )
221 return;
222 else if( aWidth == USE_DEFAULT_LINE_WIDTH )
223 aWidth = m_renderSettings->GetDefaultPenWidth();
224
225 if( aWidth == 0 )
226 aWidth = 1;
227
228 wxASSERT_MSG( aWidth > 0, "Plotter called to set negative pen width" );
229
230 if( aWidth != m_currentPenWidth )
231 fmt::println( m_workFile, "{} w", encodeDoubleForPlotter( userToDeviceSize( aWidth ) ) );
232
233 m_currentPenWidth = aWidth;
234}
235
236
237void PDF_PLOTTER::emitSetRGBColor( double r, double g, double b, double a )
238{
239 wxASSERT( m_workFile );
240
241 // PDF treats all colors as opaque, so the best we can do with alpha is generate an
242 // appropriate blended color assuming white paper.
243 if( a < 1.0 )
244 {
245 r = ( r * a ) + ( 1 - a );
246 g = ( g * a ) + ( 1 - a );
247 b = ( b * a ) + ( 1 - a );
248 }
249
250 fmt::println( m_workFile, "{} {} {} rg {} {} {} RG",
253}
254
255
256void PDF_PLOTTER::SetDash( int aLineWidth, LINE_STYLE aLineStyle )
257{
258 wxASSERT( m_workFile );
259
260 switch( aLineStyle )
261 {
262 case LINE_STYLE::DASH:
263 fmt::println( m_workFile, "[{} {}] 0 d",
264 (int) GetDashMarkLenIU( aLineWidth ),
265 (int) GetDashGapLenIU( aLineWidth ) );
266 break;
267
268 case LINE_STYLE::DOT:
269 fmt::println( m_workFile, "[{} {}] 0 d",
270 (int) GetDotMarkLenIU( aLineWidth ),
271 (int) GetDashGapLenIU( aLineWidth ) );
272 break;
273
275 fmt::println( m_workFile, "[{} {} {} {}] 0 d",
276 (int) GetDashMarkLenIU( aLineWidth ),
277 (int) GetDashGapLenIU( aLineWidth ),
278 (int) GetDotMarkLenIU( aLineWidth ),
279 (int) GetDashGapLenIU( aLineWidth ) );
280 break;
281
283 fmt::println( m_workFile, "[{} {} {} {} {} {}] 0 d",
284 (int) GetDashMarkLenIU( aLineWidth ),
285 (int) GetDashGapLenIU( aLineWidth ),
286 (int) GetDotMarkLenIU( aLineWidth ),
287 (int) GetDashGapLenIU( aLineWidth ),
288 (int) GetDotMarkLenIU( aLineWidth ),
289 (int) GetDashGapLenIU( aLineWidth ) );
290 break;
291
292 default:
293 fmt::println( m_workFile, "[] 0 d\n" );
294 }
295}
296
297
298void PDF_PLOTTER::Rect( const VECTOR2I& p1, const VECTOR2I& p2, FILL_T fill, int width,
299 int aCornerRadius )
300{
301 wxASSERT( m_workFile );
302
303 if( fill == FILL_T::NO_FILL && width == 0 )
304 return;
305
306 SetCurrentLineWidth( width );
307
308 if( aCornerRadius > 0 )
309 {
310 BOX2I box( p1, VECTOR2I( p2.x - p1.x, p2.y - p1.y ) );
311 box.Normalize();
312 SHAPE_RECT rect( box );
313 rect.SetRadius( aCornerRadius );
314 PlotPoly( rect.Outline(), fill, width, nullptr );
315 return;
316 }
317
318 VECTOR2I size = p2 - p1;
319
320 if( size.x == 0 && size.y == 0 )
321 {
322 // Can't draw zero-sized rectangles
323 MoveTo( VECTOR2I( p1.x, p1.y ) );
324 FinishTo( VECTOR2I( p1.x, p1.y ) );
325
326 return;
327 }
328
329 if( std::min( std::abs( size.x ), std::abs( size.y ) ) < width )
330 {
331 // Too thick stroked rectangles are buggy, draw as polygon
332 std::vector<VECTOR2I> cornerList;
333
334 cornerList.emplace_back( p1.x, p1.y );
335 cornerList.emplace_back( p2.x, p1.y );
336 cornerList.emplace_back( p2.x, p2.y );
337 cornerList.emplace_back( p1.x, p2.y );
338 cornerList.emplace_back( p1.x, p1.y );
339
340 PlotPoly( cornerList, fill, width, nullptr );
341
342 return;
343 }
344
345 VECTOR2D p1_dev = userToDeviceCoordinates( p1 );
346 VECTOR2D p2_dev = userToDeviceCoordinates( p2 );
347
348 char paintOp;
349
350 if( fill == FILL_T::NO_FILL )
351 paintOp = 'S';
352 else
353 paintOp = width > 0 ? 'B' : 'f';
354
355 fmt::println( m_workFile, "{} {} {} {} re {}",
356 encodeDoubleForPlotter( p1_dev.x ),
357 encodeDoubleForPlotter( p1_dev.y ),
358 encodeDoubleForPlotter( p2_dev.x - p1_dev.x ),
359 encodeDoubleForPlotter( p2_dev.y - p1_dev.y ),
360 paintOp );
361}
362
363
364void PDF_PLOTTER::Circle( const VECTOR2I& pos, int diametre, FILL_T aFill, int width )
365{
366 wxASSERT( m_workFile );
367
368 if( aFill == FILL_T::NO_FILL && width == 0 )
369 return;
370
371 SetCurrentLineWidth( width );
372
373 VECTOR2D pos_dev = userToDeviceCoordinates( pos );
374 double radius = userToDeviceSize( diametre / 2.0 );
375
376 // If diameter is less than width, switch to filled mode
377 if( aFill == FILL_T::NO_FILL && diametre < GetCurrentLineWidth() )
378 {
379 aFill = FILL_T::FILLED_SHAPE;
380 radius = userToDeviceSize( ( diametre / 2.0 ) + ( width / 2.0 ) );
381 }
382
383 /* OK. Here's a trick. PDF doesn't support circles or circular angles, that's
384 a fact. You'll have to do with cubic beziers. These *can't* represent
385 circular arcs (NURBS can, beziers don't). But there is a widely known
386 approximation which is really good
387 */
388
389 double magic = radius * 0.551784; // You don't want to know where this come from
390
391 // This is the convex hull for the bezier approximated circle
392 fmt::println( m_workFile,
393 "{} {} m "
394 "{} {} {} {} {} {} c "
395 "{} {} {} {} {} {} c "
396 "{} {} {} {} {} {} c "
397 "{} {} {} {} {} {} c {}",
398 encodeDoubleForPlotter( pos_dev.x - radius ), encodeDoubleForPlotter( pos_dev.y ),
399
400 encodeDoubleForPlotter( pos_dev.x - radius ), encodeDoubleForPlotter( pos_dev.y + magic ),
401 encodeDoubleForPlotter( pos_dev.x - magic ), encodeDoubleForPlotter( pos_dev.y + radius ),
402 encodeDoubleForPlotter( pos_dev.x ), encodeDoubleForPlotter( pos_dev.y + radius ),
403
404 encodeDoubleForPlotter( pos_dev.x + magic ), encodeDoubleForPlotter( pos_dev.y + radius ),
405 encodeDoubleForPlotter( pos_dev.x + radius ), encodeDoubleForPlotter( pos_dev.y + magic ),
406 encodeDoubleForPlotter( pos_dev.x + radius ), encodeDoubleForPlotter( pos_dev.y ),
407
408 encodeDoubleForPlotter( pos_dev.x + radius ), encodeDoubleForPlotter( pos_dev.y - magic ),
409 encodeDoubleForPlotter( pos_dev.x + magic ), encodeDoubleForPlotter( pos_dev.y - radius ),
410 encodeDoubleForPlotter( pos_dev.x ), encodeDoubleForPlotter( pos_dev.y - radius ),
411
412 encodeDoubleForPlotter( pos_dev.x - magic ), encodeDoubleForPlotter( pos_dev.y - radius ),
413 encodeDoubleForPlotter( pos_dev.x - radius ), encodeDoubleForPlotter( pos_dev.y - magic ),
414 encodeDoubleForPlotter( pos_dev.x - radius ), encodeDoubleForPlotter( pos_dev.y ),
415
416 aFill == FILL_T::NO_FILL ? 's' : 'b' );
417}
418
419
420std::vector<VECTOR2D> PDF_PLOTTER::arcPath( const VECTOR2D& aCenter, const EDA_ANGLE& aStartAngle,
421 const EDA_ANGLE& aAngle, double aRadius )
422{
423 std::vector<VECTOR2D> path;
424
425 /*
426 * Arcs are not so easily approximated by beziers (in the general case), so we approximate
427 * them in the old way
428 */
429 EDA_ANGLE startAngle = -aStartAngle;
430 EDA_ANGLE endAngle = startAngle - aAngle;
431 VECTOR2I start;
433 const EDA_ANGLE delta( 5, DEGREES_T ); // increment to draw circles
434
435 if( startAngle > endAngle )
436 std::swap( startAngle, endAngle );
437
438 // Usual trig arc plotting routine...
439 start.x = KiROUND( aCenter.x + aRadius * ( -startAngle ).Cos() );
440 start.y = KiROUND( aCenter.y + aRadius * ( -startAngle ).Sin() );
441 path.emplace_back( userToDeviceCoordinates( start ) );
442
443 for( EDA_ANGLE ii = startAngle + delta; ii < endAngle; ii += delta )
444 {
445 end.x = KiROUND( aCenter.x + aRadius * ( -ii ).Cos() );
446 end.y = KiROUND( aCenter.y + aRadius * ( -ii ).Sin() );
447 path.emplace_back( userToDeviceCoordinates( end ) );
448 }
449
450 end.x = KiROUND( aCenter.x + aRadius * ( -endAngle ).Cos() );
451 end.y = KiROUND( aCenter.y + aRadius * ( -endAngle ).Sin() );
452 path.emplace_back( userToDeviceCoordinates( end ) );
453
454 return path;
455}
456
457
458void PDF_PLOTTER::Arc( const VECTOR2D& aCenter, const EDA_ANGLE& aStartAngle,
459 const EDA_ANGLE& aAngle, double aRadius, FILL_T aFill, int aWidth )
460{
461 wxASSERT( m_workFile );
462
463 SetCurrentLineWidth( aWidth );
464
465 if( aRadius <= 0 )
466 {
468 return;
469 }
470
471 std::vector<VECTOR2D> path = arcPath( aCenter, aStartAngle, aAngle, aRadius );
472
473 if( path.size() >= 2 )
474 {
475 fmt::print( m_workFile, "{} {} m ",
477
478 for( int ii = 1; ii < (int) path.size(); ++ii )
479 fmt::print( m_workFile, "{} {} l ",
481 }
482
483 // The arc is drawn... if not filled we stroke it, otherwise we finish
484 // closing the pie at the center
485 if( aFill == FILL_T::NO_FILL )
486 {
487 fmt::println( m_workFile, "S" );
488 }
489 else
490 {
491 VECTOR2D pos_dev = userToDeviceCoordinates( aCenter );
492 fmt::println( m_workFile, "{} {} l b",
493 encodeDoubleForPlotter( pos_dev.x ), encodeDoubleForPlotter( pos_dev.y ) );
494 }
495}
496
497
498void PDF_PLOTTER::PlotPoly( const std::vector<VECTOR2I>& aCornerList, FILL_T aFill, int aWidth,
499 void* aData )
500{
501 wxASSERT( m_workFile );
502
503 if( aCornerList.size() <= 1 )
504 return;
505
506 if( aFill == FILL_T::NO_FILL && aWidth == 0 )
507 return;
508
509 SetCurrentLineWidth( aWidth );
510
511 VECTOR2D pos = userToDeviceCoordinates( aCornerList[0] );
512 fmt::print( m_workFile, "{:f} {:f} m ", pos.x, pos.y );
513
514 for( unsigned ii = 1; ii < aCornerList.size(); ii++ )
515 {
516 pos = userToDeviceCoordinates( aCornerList[ii] );
517 fmt::print( m_workFile, "{:f} {:f} l ", pos.x, pos.y );
518 }
519
520 // Close path and stroke and/or fill
521 if( aFill == FILL_T::NO_FILL )
522 fmt::println( m_workFile, "S" );
523 else if( aWidth == 0 )
524 fmt::println( m_workFile, "h f" );
525 else
526 fmt::println( m_workFile, "b" );
527}
528
529
530void PDF_PLOTTER::PlotPoly( const SHAPE_LINE_CHAIN& aLineChain, FILL_T aFill, int aWidth, void* aData )
531{
532 SetCurrentLineWidth( aWidth );
533
534 std::set<size_t> handledArcs;
535 std::vector<VECTOR2D> path;
536
537 for( int ii = 0; ii < aLineChain.SegmentCount(); ++ii )
538 {
539 if( aLineChain.IsArcSegment( ii ) )
540 {
541 size_t arcIndex = aLineChain.ArcIndex( ii );
542
543 if( !handledArcs.contains( arcIndex ) )
544 {
545 handledArcs.insert( arcIndex );
546 const SHAPE_ARC& arc( aLineChain.Arc( arcIndex ) );
547 std::vector<VECTOR2D> arc_path = arcPath( arc.GetCenter(), arc.GetStartAngle(),
548 arc.GetCentralAngle(), arc.GetRadius() );
549
550 for( const VECTOR2D& pt : std::ranges::reverse_view( arc_path ) )
551 path.emplace_back( pt );
552 }
553 }
554 else
555 {
556 const SEG& seg( aLineChain.Segment( ii ) );
557 path.emplace_back( userToDeviceCoordinates( seg.A ) );
558 path.emplace_back( userToDeviceCoordinates( seg.B ) );
559 }
560 }
561
562 if( path.size() <= 1 )
563 return;
564
565 fmt::print( m_workFile, "{} {} m ",
567
568 for( int ii = 1; ii < (int) path.size(); ++ii )
569 fmt::print( m_workFile, "{} {} l ",
571
572 // Close path and stroke and/or fill
573 if( aFill == FILL_T::NO_FILL )
574 fmt::println( m_workFile, "S" );
575 else if( aWidth == 0 )
576 fmt::println( m_workFile, "h f" );
577 else
578 fmt::println( m_workFile, "b" );
579}
580
581
582void PDF_PLOTTER::PenTo( const VECTOR2I& pos, char plume )
583{
584 wxASSERT( m_workFile );
585
586 if( plume == 'Z' )
587 {
588 if( m_penState != 'Z' )
589 {
590 fmt::println( m_workFile, "S" );
591 m_penState = 'Z';
592 m_penLastpos.x = -1;
593 m_penLastpos.y = -1;
594 }
595
596 return;
597 }
598
599 if( m_penState != plume || pos != m_penLastpos )
600 {
601 VECTOR2D pos_dev = userToDeviceCoordinates( pos );
602 fmt::println( m_workFile, "{:f} {:f} {}",
603 pos_dev.x,
604 pos_dev.y,
605 plume == 'D' ? 'l' : 'm' );
606 }
607
608 m_penState = plume;
609 m_penLastpos = pos;
610}
611
612
613void PDF_PLOTTER::PlotImage( const wxImage& aImage, const VECTOR2I& aPos, double aScaleFactor )
614{
615 wxASSERT( m_workFile );
616 VECTOR2I pix_size( aImage.GetWidth(), aImage.GetHeight() );
617
618 // Requested size (in IUs)
619 VECTOR2D drawsize( aScaleFactor * pix_size.x, aScaleFactor * pix_size.y );
620
621 // calculate the bitmap start position
622 VECTOR2I start( aPos.x - drawsize.x / 2, aPos.y + drawsize.y / 2 );
623 VECTOR2D dev_start = userToDeviceCoordinates( start );
624
625 // Deduplicate images
626 auto findHandleForImage =
627 [&]( const wxImage& aCurrImage ) -> int
628 {
629 for( const auto& [imgHandle, image] : m_imageHandles )
630 {
631 if( image.IsSameAs( aCurrImage ) )
632 return imgHandle;
633
634 if( image.GetWidth() != aCurrImage.GetWidth() )
635 continue;
636
637 if( image.GetHeight() != aCurrImage.GetHeight() )
638 continue;
639
640 if( image.GetType() != aCurrImage.GetType() )
641 continue;
642
643 if( image.HasAlpha() != aCurrImage.HasAlpha() )
644 continue;
645
646 if( image.HasMask() != aCurrImage.HasMask()
647 || image.GetMaskRed() != aCurrImage.GetMaskRed()
648 || image.GetMaskGreen() != aCurrImage.GetMaskGreen()
649 || image.GetMaskBlue() != aCurrImage.GetMaskBlue() )
650 continue;
651
652 int pixCount = image.GetWidth() * image.GetHeight();
653
654 if( memcmp( image.GetData(), aCurrImage.GetData(), pixCount * 3 ) != 0 )
655 continue;
656
657 if( image.HasAlpha() && memcmp( image.GetAlpha(), aCurrImage.GetAlpha(), pixCount ) != 0 )
658 continue;
659
660 return imgHandle;
661 }
662
663 return -1;
664 };
665
666 int imgHandle = findHandleForImage( aImage );
667
668 if( imgHandle == -1 )
669 {
670 imgHandle = allocPdfObject();
671 m_imageHandles.emplace( imgHandle, aImage );
672 }
673
674 /* PDF has an uhm... simplified coordinate system handling. There is
675 *one* operator to do everything (the PS concat equivalent). At least
676 they kept the matrix stack to save restore environments. Also images
677 are always emitted at the origin with a size of 1x1 user units.
678 What we need to do is:
679 1) save the CTM end establish the new one
680 2) plot the image
681 3) restore the CTM
682 4) profit
683 */
684 fmt::println( m_workFile, "q {} 0 0 {} {} {} cm", // Step 1
687 encodeDoubleForPlotter( dev_start.x ),
688 encodeDoubleForPlotter( dev_start.y ) );
689
690 fmt::println( m_workFile, "/Im{} Do", imgHandle );
691 fmt::println( m_workFile, "Q" );
692}
693
694
696{
697 m_xrefTable.push_back( 0 );
698 return m_xrefTable.size() - 1;
699}
700
701
703{
704 wxASSERT( m_outputFile );
705 wxASSERT( !m_workFile );
706
707 if( aHandle < 0 )
708 aHandle = allocPdfObject();
709
710 m_xrefTable[aHandle] = ftell( m_outputFile );
711 fmt::println( m_outputFile, "{} 0 obj", aHandle );
712 return aHandle;
713}
714
715
717{
718 wxASSERT( m_outputFile );
719 wxASSERT( !m_workFile );
720 fmt::println( m_outputFile, "endobj" );
721}
722
723
725{
726 wxASSERT( m_outputFile );
727 wxASSERT( !m_workFile );
728 int handle = startPdfObject( aHandle );
729
730 // This is guaranteed to be handle+1 but needs to be allocated since
731 // you could allocate more object during stream preparation
733
734 if( ADVANCED_CFG::GetCfg().m_DebugPDFWriter )
735 {
736 fmt::print( m_outputFile,
737 "<< /Length {} 0 R >>\nstream\n",
739 }
740 else
741 {
742 fmt::print( m_outputFile,
743 "<< /Length {} 0 R /Filter /FlateDecode >>\nstream\n",
745 }
746
747 // Open a temporary file to accumulate the stream
748 m_workFilename = wxFileName::CreateTempFileName( "" );
749 m_workFile = wxFopen( m_workFilename, wxT( "w+b" ) );
750 wxASSERT( m_workFile );
751 return handle;
752}
753
754
756{
757 wxASSERT( m_workFile );
758
759 long stream_len = ftell( m_workFile );
760
761 if( stream_len < 0 )
762 {
763 wxASSERT( false );
764 return;
765 }
766
767 // Rewind the file, read in the page stream and DEFLATE it
768 fseek( m_workFile, 0, SEEK_SET );
769 unsigned char *inbuf = new unsigned char[stream_len];
770
771 int rc = fread( inbuf, 1, stream_len, m_workFile );
772 wxASSERT( rc == stream_len );
773 ignore_unused( rc );
774
775 // We are done with the temporary file, junk it
776 fclose( m_workFile );
777 m_workFile = nullptr;
778 ::wxRemoveFile( m_workFilename );
779
780 unsigned out_count;
781
782 if( ADVANCED_CFG::GetCfg().m_DebugPDFWriter )
783 {
784 out_count = stream_len;
785 fwrite( inbuf, out_count, 1, m_outputFile );
786 }
787 else
788 {
789 // NULL means memos owns the memory, but provide a hint on optimum size needed.
790 wxMemoryOutputStream memos( nullptr, std::max( 2000l, stream_len ) ) ;
791
792 {
793 /* Somewhat standard parameters to compress in DEFLATE. The PDF spec is
794 * misleading, it says it wants a DEFLATE stream but it really want a ZLIB
795 * stream! (a DEFLATE stream would be generated with -15 instead of 15)
796 * rc = deflateInit2( &zstrm, Z_BEST_COMPRESSION, Z_DEFLATED, 15,
797 * 8, Z_DEFAULT_STRATEGY );
798 */
799
800 wxZlibOutputStream zos( memos, wxZ_BEST_COMPRESSION, wxZLIB_ZLIB );
801
802 zos.Write( inbuf, stream_len );
803 } // flush the zip stream using zos destructor
804
805 wxStreamBuffer* sb = memos.GetOutputStreamBuffer();
806
807 out_count = sb->Tell();
808 fwrite( sb->GetBufferStart(), 1, out_count, m_outputFile );
809 }
810
811 delete[] inbuf;
812 fmt::print( m_outputFile, "\nendstream\n" );
814
815 // Writing the deferred length as an indirect object
817 fmt::println( m_outputFile, "{}", out_count );
819}
820
821
822void PDF_PLOTTER::StartPage( const wxString& aPageNumber, const wxString& aPageName,
823 const wxString& aParentPageNumber, const wxString& aParentPageName )
824{
825 wxASSERT( m_outputFile );
826 wxASSERT( !m_workFile );
827
828 m_pageNumbers.push_back( aPageNumber );
829 m_pageName = aPageName.IsEmpty() ? wxString::Format( _( "Page %s" ),
830 aPageNumber )
831 : wxString::Format( _( "%s (Page %s)" ),
832 aPageName,
833 aPageNumber );
834 m_parentPageName = aParentPageName.IsEmpty() ? wxString::Format( _( "Page %s" ),
835 aParentPageNumber )
836 : wxString::Format( _( "%s (Page %s)" ),
837 aParentPageName,
838 aParentPageNumber );
839
840 // Compute the paper size in IUs
841 m_paperSize = m_pageInfo.GetSizeMils();
842 m_paperSize.x *= 10.0 / m_iuPerDeviceUnit;
843 m_paperSize.y *= 10.0 / m_iuPerDeviceUnit;
844
845 // Set m_currentPenWidth to a unused value to ensure the pen width
846 // will be initialized to a the right value in pdf file by the first item to plot
848
849 if( !m_3dExportMode )
850 {
851 // Open the content stream; the page object will go later
853
854 /* Now, until ClosePage *everything* must be wrote in workFile, to be
855 compressed later in closePdfStream */
856
857 // Default graphic settings (coordinate system, default color and line style)
858 fmt::println( m_workFile,
859 "{} 0 0 {} 0 0 cm 1 J 1 j 0 0 0 rg 0 0 0 RG {} w",
862 encodeDoubleForPlotter( userToDeviceSize( m_renderSettings->GetDefaultPenWidth() ) ) );
863 }
864}
865
866
867void WriteImageStream( const wxImage& aImage, wxDataOutputStream& aOut, wxColor bg, bool colorMode )
868{
869 int w = aImage.GetWidth();
870 int h = aImage.GetHeight();
871
872 for( int y = 0; y < h; y++ )
873 {
874 for( int x = 0; x < w; x++ )
875 {
876 unsigned char r = aImage.GetRed( x, y ) & 0xFF;
877 unsigned char g = aImage.GetGreen( x, y ) & 0xFF;
878 unsigned char b = aImage.GetBlue( x, y ) & 0xFF;
879
880 if( aImage.HasMask() )
881 {
882 if( r == aImage.GetMaskRed() && g == aImage.GetMaskGreen()
883 && b == aImage.GetMaskBlue() )
884 {
885 r = bg.Red();
886 g = bg.Green();
887 b = bg.Blue();
888 }
889 }
890
891 if( colorMode )
892 {
893 aOut.Write8( r );
894 aOut.Write8( g );
895 aOut.Write8( b );
896 }
897 else
898 {
899 // Greyscale conversion (CIE 1931)
900 unsigned char grey = KiROUND( r * 0.2126 + g * 0.7152 + b * 0.0722 );
901
902 aOut.Write8( grey );
903 }
904 }
905 }
906}
907
908
909void WriteImageSMaskStream( const wxImage& aImage, wxDataOutputStream& aOut )
910{
911 int w = aImage.GetWidth();
912 int h = aImage.GetHeight();
913
914 if( aImage.HasMask() )
915 {
916 for( int y = 0; y < h; y++ )
917 {
918 for( int x = 0; x < w; x++ )
919 {
920 unsigned char a = 255;
921 unsigned char r = aImage.GetRed( x, y );
922 unsigned char g = aImage.GetGreen( x, y );
923 unsigned char b = aImage.GetBlue( x, y );
924
925 if( r == aImage.GetMaskRed() && g == aImage.GetMaskGreen()
926 && b == aImage.GetMaskBlue() )
927 {
928 a = 0;
929 }
930
931 aOut.Write8( a );
932 }
933 }
934 }
935 else if( aImage.HasAlpha() )
936 {
937 int size = w * h;
938 aOut.Write8( aImage.GetAlpha(), size );
939 }
940}
941
942
944{
945 // non 3d exports need this
946 if( m_pageStreamHandle != -1 )
947 {
948 wxASSERT( m_workFile );
949
950 // Close the page stream (and compress it)
952 }
953
954 // Page size is in 1/72 of inch (default user space units). Works like the bbox in postscript
955 // but there is no need for swapping the sizes, since PDF doesn't require a portrait page.
956 // We use the MediaBox but PDF has lots of other less-used boxes that could be used.
957 const double PTsPERMIL = 0.072;
958 VECTOR2D psPaperSize = VECTOR2D( m_pageInfo.GetSizeMils() ) * PTsPERMIL;
959
960 auto iuToPdfUserSpace =
961 [&]( const VECTOR2I& aCoord ) -> VECTOR2D
962 {
963 VECTOR2D pos = VECTOR2D( aCoord ) * PTsPERMIL / ( m_IUsPerDecimil * 10 );
964
965 // PDF y=0 is at bottom of page, invert coordinate
966 VECTOR2D retval( pos.x, psPaperSize.y - pos.y );
967
968 // The pdf plot can be mirrored (from left to right). So mirror the
969 // x coordinate if m_plotMirror is set
970 if( m_plotMirror )
971 {
973 retval.x = ( psPaperSize.x - pos.x );
974 else
975 retval.y = pos.y;
976 }
977
978 return retval;
979 };
980
981 // Handle annotations (at the moment only "link" type objects)
982 std::vector<int> annotHandles;
983
984 // Allocate all hyperlink objects for the page and calculate their position in user space
985 // coordinates
986 for( const std::pair<BOX2I, wxString>& linkPair : m_hyperlinksInPage )
987 {
988 const BOX2I& box = linkPair.first;
989 const wxString& url = linkPair.second;
990
991 VECTOR2D bottomLeft = iuToPdfUserSpace( box.GetPosition() );
992 VECTOR2D topRight = iuToPdfUserSpace( box.GetEnd() );
993
994 BOX2D userSpaceBox;
995 userSpaceBox.SetOrigin( bottomLeft );
996 userSpaceBox.SetEnd( topRight );
997
998 annotHandles.push_back( allocPdfObject() );
999
1000 m_hyperlinkHandles.insert( { annotHandles.back(), { userSpaceBox, url } } );
1001 }
1002
1003 for( const std::pair<BOX2I, std::vector<wxString>>& menuPair : m_hyperlinkMenusInPage )
1004 {
1005 const BOX2I& box = menuPair.first;
1006 const std::vector<wxString>& urls = menuPair.second;
1007
1008 VECTOR2D bottomLeft = iuToPdfUserSpace( box.GetPosition() );
1009 VECTOR2D topRight = iuToPdfUserSpace( box.GetEnd() );
1010
1011 BOX2D userSpaceBox;
1012 userSpaceBox.SetOrigin( bottomLeft );
1013 userSpaceBox.SetEnd( topRight );
1014
1015 annotHandles.push_back( allocPdfObject() );
1016
1017 m_hyperlinkMenuHandles.insert( { annotHandles.back(), { userSpaceBox, urls } } );
1018 }
1019
1020 int annot3DHandle = -1;
1021
1022 if( m_3dExportMode )
1023 {
1024 annot3DHandle = allocPdfObject();
1025 annotHandles.push_back( annot3DHandle );
1026 }
1027
1028
1029 int annotArrayHandle = -1;
1030
1031 // If we have added any annotation links, create an array containing all the objects
1032 if( annotHandles.size() > 0 )
1033 {
1034 annotArrayHandle = startPdfObject();
1035 bool isFirst = true;
1036
1037 fmt::print( m_outputFile, "[" );
1038
1039 for( int handle : annotHandles )
1040 {
1041 if( isFirst )
1042 isFirst = false;
1043 else
1044 fmt::print( m_outputFile, " " );
1045
1046 fmt::print( m_outputFile, "{} 0 R", handle );
1047 }
1048
1049 fmt::println( m_outputFile, "]" );
1051 }
1052
1053 // Emit the page object and put it in the page list for later
1054 int pageHandle = startPdfObject();
1055 m_pageHandles.push_back( pageHandle );
1056
1057 fmt::print( m_outputFile,
1058 "<<\n"
1059 "/Type /Page\n"
1060 "/Parent {} 0 R\n"
1061 "/Resources <<\n"
1062 " /ProcSet [/PDF /Text /ImageC /ImageB]\n"
1063 " /Font {} 0 R\n"
1064 " /XObject {} 0 R >>\n"
1065 "/MediaBox [0 0 {} {}]\n",
1069 encodeDoubleForPlotter( psPaperSize.x ),
1070 encodeDoubleForPlotter( psPaperSize.y ) );
1071
1072 if( m_pageStreamHandle != -1 )
1073 fmt::print( m_outputFile, "/Contents {} 0 R\n", m_pageStreamHandle );
1074
1075 if( annotHandles.size() > 0 )
1076 fmt::print( m_outputFile, "/Annots {} 0 R", annotArrayHandle );
1077
1078 fmt::print( m_outputFile, ">>\n" );
1079
1081
1082 if( m_3dExportMode )
1083 {
1084 startPdfObject( annot3DHandle );
1085 fmt::print( m_outputFile,
1086 "<<\n"
1087 "/Type /Annot\n"
1088 "/Subtype /3D\n"
1089 "/Rect [0 0 {} {}]\n"
1090 "/NM (3D Annotation)\n"
1091 "/3DD {} 0 R\n"
1092 "/3DV 0\n"
1093 "/3DA<</A/PO/D/PC/TB true/NP true>>\n"
1094 "/3DI true\n"
1095 "/P {} 0 R\n"
1096 ">>\n",
1097 encodeDoubleForPlotter( psPaperSize.x ), encodeDoubleForPlotter( psPaperSize.y ),
1098 m_3dModelHandle, pageHandle );
1099
1101 }
1102
1103 // Mark the page stream as idle
1104 m_pageStreamHandle = -1;
1105
1106 int actionHandle = emitGoToAction( pageHandle );
1107 PDF_PLOTTER::OUTLINE_NODE* parent_node = m_outlineRoot.get();
1108
1109 if( !m_parentPageName.IsEmpty() )
1110 {
1111 // Search for the parent node iteratively through the entire tree
1112 std::stack<OUTLINE_NODE*> nodes;
1113 nodes.push( m_outlineRoot.get() );
1114
1115 while( !nodes.empty() )
1116 {
1117 OUTLINE_NODE* node = nodes.top();
1118 nodes.pop();
1119
1120 // Check if this node matches
1121 if( node->title == m_parentPageName )
1122 {
1123 parent_node = node;
1124 break;
1125 }
1126
1127 // Add all children to the stack
1128 for( OUTLINE_NODE* child : node->children )
1129 nodes.push( child );
1130 }
1131 }
1132
1133 OUTLINE_NODE* pageOutlineNode = addOutlineNode( parent_node, actionHandle, m_pageName );
1134
1135 // let's reorg the symbol bookmarks under a page handle
1136 // let's reorg the symbol bookmarks under a page handle
1137 for( const auto& [groupName, groupVector] : m_bookmarksInPage )
1138 {
1139 OUTLINE_NODE* groupOutlineNode = addOutlineNode( pageOutlineNode, actionHandle, groupName );
1140
1141 for( const std::pair<BOX2I, wxString>& bookmarkPair : groupVector )
1142 {
1143 const BOX2I& box = bookmarkPair.first;
1144 const wxString& ref = bookmarkPair.second;
1145
1146 VECTOR2I bottomLeft = iuToPdfUserSpace( box.GetPosition() );
1147 VECTOR2I topRight = iuToPdfUserSpace( box.GetEnd() );
1148
1149 actionHandle = emitGoToAction( pageHandle, bottomLeft, topRight );
1150
1151 addOutlineNode( groupOutlineNode, actionHandle, ref );
1152 }
1153
1154 std::sort( groupOutlineNode->children.begin(), groupOutlineNode->children.end(),
1155 []( const OUTLINE_NODE* a, const OUTLINE_NODE* b ) -> bool
1156 {
1157 return a->title < b->title;
1158 } );
1159 }
1160
1161 // Clean up
1162 m_hyperlinksInPage.clear();
1163 m_hyperlinkMenusInPage.clear();
1164 m_bookmarksInPage.clear();
1165}
1166
1167
1168bool PDF_PLOTTER::StartPlot( const wxString& aPageNumber )
1169{
1170 return StartPlot( aPageNumber, wxEmptyString );
1171}
1172
1173
1174bool PDF_PLOTTER::StartPlot( const wxString& aPageNumber, const wxString& aPageName )
1175{
1176 wxASSERT( m_outputFile );
1177
1178 // First things first: the customary null object
1179 m_xrefTable.clear();
1180 m_xrefTable.push_back( 0 );
1181 m_hyperlinksInPage.clear();
1182 m_hyperlinkMenusInPage.clear();
1183 m_hyperlinkHandles.clear();
1184 m_hyperlinkMenuHandles.clear();
1185 m_bookmarksInPage.clear();
1187
1188 m_outlineRoot = std::make_unique<OUTLINE_NODE>();
1189
1190 if( !m_strokeFontManager )
1191 m_strokeFontManager = std::make_unique<PDF_STROKE_FONT_MANAGER>();
1192 else
1193 m_strokeFontManager->Reset();
1194
1196 m_outlineFontManager = std::make_unique<PDF_OUTLINE_FONT_MANAGER>();
1197 else
1198 m_outlineFontManager->Reset();
1199
1200 /* The header (that's easy!). The second line is binary junk required
1201 to make the file binary from the beginning (the important thing is
1202 that they must have the bit 7 set) */
1203 fmt::print( m_outputFile, "%PDF-1.5\n%\200\201\202\203\n" );
1204
1205 /* Allocate an entry for the page tree root, it will go in every page parent entry */
1207
1208 /* In the same way, the font resource dictionary is used by every page
1209 (it *could* be inherited via the Pages tree */
1211
1213
1215
1216 /* Now, the PDF is read from the end, (more or less)... so we start
1217 with the page stream for page 1. Other more important stuff is written
1218 at the end */
1219 StartPage( aPageNumber, aPageName );
1220 return true;
1221}
1222
1223
1224int PDF_PLOTTER::emitGoToAction( int aPageHandle, const VECTOR2I& aBottomLeft,
1225 const VECTOR2I& aTopRight )
1226{
1227 int actionHandle = allocPdfObject();
1228 startPdfObject( actionHandle );
1229
1230 fmt::print( m_outputFile,
1231 "<</S /GoTo /D [{} 0 R /FitR {} {} {} {}]\n"
1232 ">>\n",
1233 aPageHandle,
1234 aBottomLeft.x,
1235 aBottomLeft.y,
1236 aTopRight.x,
1237 aTopRight.y );
1238
1240
1241 return actionHandle;
1242}
1243
1244
1245int PDF_PLOTTER::emitGoToAction( int aPageHandle )
1246{
1247 int actionHandle = allocPdfObject();
1248 startPdfObject( actionHandle );
1249
1250 fmt::println( m_outputFile,
1251 "<</S /GoTo /D [{} 0 R /Fit]\n"
1252 ">>",
1253 aPageHandle );
1254
1256
1257 return actionHandle;
1258}
1259
1260
1261void PDF_PLOTTER::emitOutlineNode( OUTLINE_NODE* node, int parentHandle, int nextNode,
1262 int prevNode )
1263{
1264 int nodeHandle = node->entryHandle;
1265 int prevHandle = -1;
1266 int nextHandle = -1;
1267
1268 for( std::vector<OUTLINE_NODE*>::iterator it = node->children.begin();
1269 it != node->children.end(); it++ )
1270 {
1271 if( it >= node->children.end() - 1 )
1272 {
1273 nextHandle = -1;
1274 }
1275 else
1276 {
1277 nextHandle = ( *( it + 1 ) )->entryHandle;
1278 }
1279
1280 emitOutlineNode( *it, nodeHandle, nextHandle, prevHandle );
1281
1282 prevHandle = ( *it )->entryHandle;
1283 }
1284
1285 // -1 for parentHandle is the outline root itself which is handed elsewhere.
1286 if( parentHandle != -1 )
1287 {
1288 startPdfObject( nodeHandle );
1289
1290 fmt::print( m_outputFile,
1291 "<<\n"
1292 "/Title {}\n"
1293 "/Parent {} 0 R\n",
1295 parentHandle);
1296
1297 if( nextNode > 0 )
1298 {
1299 fmt::println( m_outputFile, "/Next {} 0 R", nextNode );
1300 }
1301
1302 if( prevNode > 0 )
1303 {
1304 fmt::println( m_outputFile, "/Prev {} 0 R", prevNode );
1305 }
1306
1307 if( node->children.size() > 0 )
1308 {
1309 int32_t count = -1 * static_cast<int32_t>( node->children.size() );
1310 fmt::println( m_outputFile, "/Count {}", count );
1311 fmt::println( m_outputFile, "/First {} 0 R", node->children.front()->entryHandle );
1312 fmt::println( m_outputFile, "/Last {} 0 R", node->children.back()->entryHandle );
1313 }
1314
1315 if( node->actionHandle != -1 )
1316 {
1317 fmt::println( m_outputFile, "/A {} 0 R", node->actionHandle );
1318 }
1319
1320 fmt::println( m_outputFile, ">>" );
1322 }
1323}
1324
1325
1327 const wxString& aTitle )
1328{
1329 OUTLINE_NODE *node = aParent->AddChild( aActionHandle, aTitle, allocPdfObject() );
1331
1332 return node;
1333}
1334
1335
1337{
1338 if( m_outlineRoot->children.size() > 0 )
1339 {
1340 // declare the outline object
1341 m_outlineRoot->entryHandle = allocPdfObject();
1342
1343 emitOutlineNode( m_outlineRoot.get(), -1, -1, -1 );
1344
1345 startPdfObject( m_outlineRoot->entryHandle );
1346
1347 fmt::print( m_outputFile,
1348 "<< /Type /Outlines\n"
1349 " /Count {}\n"
1350 " /First {} 0 R\n"
1351 " /Last {} 0 R\n"
1352 ">>\n",
1354 m_outlineRoot->children.front()->entryHandle,
1355 m_outlineRoot->children.back()->entryHandle
1356 );
1357
1359
1360 return m_outlineRoot->entryHandle;
1361 }
1362
1363 return -1;
1364}
1365
1367{
1368 if( !m_strokeFontManager )
1369 return;
1370
1371 for( PDF_STROKE_FONT_SUBSET* subsetPtr : m_strokeFontManager->AllSubsets() )
1372 {
1373 PDF_STROKE_FONT_SUBSET& subset = *subsetPtr;
1374
1375 if( subset.GlyphCount() <= 1 )
1376 {
1377 subset.SetCharProcsHandle( -1 );
1378 subset.SetFontHandle( -1 );
1379 subset.SetToUnicodeHandle( -1 );
1380 continue;
1381 }
1382
1383 for( PDF_STROKE_FONT_SUBSET::GLYPH& glyph : subset.Glyphs() )
1384 {
1385 int charProcHandle = startPdfStream();
1386
1387 if( !glyph.m_stream.empty() )
1388 fmt::print( m_workFile, "{}\n", glyph.m_stream );
1389
1391 glyph.m_charProcHandle = charProcHandle;
1392 }
1393
1394 int charProcDictHandle = startPdfObject();
1395 fmt::println( m_outputFile, "<<" );
1396
1397 for( const PDF_STROKE_FONT_SUBSET::GLYPH& glyph : subset.Glyphs() )
1398 fmt::println( m_outputFile, " /{} {} 0 R", glyph.m_name, glyph.m_charProcHandle );
1399
1400 fmt::println( m_outputFile, ">>" );
1402 subset.SetCharProcsHandle( charProcDictHandle );
1403
1404 int toUnicodeHandle = startPdfStream();
1405 std::string cmap = subset.BuildToUnicodeCMap();
1406
1407 if( !cmap.empty() )
1408 fmt::print( m_workFile, "{}", cmap );
1409
1411 subset.SetToUnicodeHandle( toUnicodeHandle );
1412
1413 double fontMatrixScale = 1.0 / subset.UnitsPerEm();
1414 double minX = subset.FontBBoxMinX();
1415 double minY = subset.FontBBoxMinY();
1416 double maxX = subset.FontBBoxMaxX();
1417 double maxY = subset.FontBBoxMaxY();
1418
1419 int fontHandle = startPdfObject();
1420 fmt::print( m_outputFile,
1421 "<<\n/Type /Font\n/Subtype /Type3\n/Name {}\n/FontBBox [ {} {} {} {} ]\n",
1422 subset.ResourceName(),
1423 encodeDoubleForPlotter( minX ),
1424 encodeDoubleForPlotter( minY ),
1425 encodeDoubleForPlotter( maxX ),
1426 encodeDoubleForPlotter( maxY ) );
1427 fmt::print( m_outputFile,
1428 "/FontMatrix [ {} 0 0 {} 0 0 ]\n/CharProcs {} 0 R\n",
1429 encodeDoubleForPlotter( fontMatrixScale ),
1430 encodeDoubleForPlotter( fontMatrixScale ),
1431 subset.CharProcsHandle() );
1432 fmt::print( m_outputFile,
1433 "/Encoding << /Type /Encoding /Differences {} >>\n",
1434 subset.BuildDifferencesArray() );
1435 fmt::print( m_outputFile,
1436 "/FirstChar {}\n/LastChar {}\n/Widths {}\n",
1437 subset.FirstChar(),
1438 subset.LastChar(),
1439 subset.BuildWidthsArray() );
1440 fmt::print( m_outputFile,
1441 "/ToUnicode {} 0 R\n/Resources << /ProcSet [/PDF /Text] >>\n>>\n",
1442 subset.ToUnicodeHandle() );
1444 subset.SetFontHandle( fontHandle );
1445 }
1446}
1447
1448
1450{
1452 return;
1453
1454 for( PDF_OUTLINE_FONT_SUBSET* subsetPtr : m_outlineFontManager->AllSubsets() )
1455 {
1456 if( !subsetPtr || !subsetPtr->HasGlyphs() )
1457 continue;
1458
1459 const std::vector<uint8_t>& fontData = subsetPtr->FontFileData();
1460
1461 if( fontData.empty() )
1462 continue;
1463
1464 int fontFileHandle = startPdfStream();
1465 subsetPtr->SetFontFileHandle( fontFileHandle );
1466
1467 if( !fontData.empty() )
1468 fwrite( fontData.data(), fontData.size(), 1, m_workFile );
1469
1471
1472 std::string cidMap = subsetPtr->BuildCIDToGIDStream();
1473 int cidMapHandle = startPdfStream();
1474 subsetPtr->SetCIDMapHandle( cidMapHandle );
1475
1476 if( !cidMap.empty() )
1477 fwrite( cidMap.data(), cidMap.size(), 1, m_workFile );
1478
1480
1481 std::string toUnicode = subsetPtr->BuildToUnicodeCMap();
1482 int toUnicodeHandle = startPdfStream();
1483 subsetPtr->SetToUnicodeHandle( toUnicodeHandle );
1484
1485 if( !toUnicode.empty() )
1486 fmt::print( m_workFile, "{}", toUnicode );
1487
1489
1490 int descriptorHandle = startPdfObject();
1491 subsetPtr->SetFontDescriptorHandle( descriptorHandle );
1492
1493 fmt::print( m_outputFile,
1494 "<<\n/Type /FontDescriptor\n/FontName /{}\n/Flags {}\n/ItalicAngle {}\n/Ascent {}\n/Descent {}\n"
1495 "/CapHeight {}\n/StemV {}\n/FontBBox [ {} {} {} {} ]\n/FontFile2 {} 0 R\n>>\n",
1496 subsetPtr->BaseFontName(),
1497 subsetPtr->Flags(),
1498 encodeDoubleForPlotter( subsetPtr->ItalicAngle() ),
1499 encodeDoubleForPlotter( subsetPtr->Ascent() ),
1500 encodeDoubleForPlotter( subsetPtr->Descent() ),
1501 encodeDoubleForPlotter( subsetPtr->CapHeight() ),
1502 encodeDoubleForPlotter( subsetPtr->StemV() ),
1503 encodeDoubleForPlotter( subsetPtr->BBoxMinX() ),
1504 encodeDoubleForPlotter( subsetPtr->BBoxMinY() ),
1505 encodeDoubleForPlotter( subsetPtr->BBoxMaxX() ),
1506 encodeDoubleForPlotter( subsetPtr->BBoxMaxY() ),
1507 subsetPtr->FontFileHandle() );
1509
1510 int cidFontHandle = startPdfObject();
1511 subsetPtr->SetCIDFontHandle( cidFontHandle );
1512
1513 std::string widths = subsetPtr->BuildWidthsArray();
1514
1515 fmt::print( m_outputFile,
1516 "<<\n/Type /Font\n/Subtype /CIDFontType2\n/BaseFont /{}\n"
1517 "/CIDSystemInfo << /Registry (Adobe) /Ordering (Identity) /Supplement 0 >>\n"
1518 "/FontDescriptor {} 0 R\n/W {}\n/CIDToGIDMap {} 0 R\n>>\n",
1519 subsetPtr->BaseFontName(),
1520 subsetPtr->FontDescriptorHandle(),
1521 widths,
1522 subsetPtr->CIDMapHandle() );
1524
1525 int fontHandle = startPdfObject();
1526 subsetPtr->SetFontHandle( fontHandle );
1527
1528 fmt::print( m_outputFile,
1529 "<<\n/Type /Font\n/Subtype /Type0\n/BaseFont /{}\n/Encoding /Identity-H\n"
1530 "/DescendantFonts [ {} 0 R ]\n/ToUnicode {} 0 R\n>>\n",
1531 subsetPtr->BaseFontName(),
1532 subsetPtr->CIDFontHandle(),
1533 subsetPtr->ToUnicodeHandle() );
1535 }
1536}
1537
1538
1540{
1543
1545 fmt::println( m_outputFile, "<<" );
1546
1548 {
1549 for( PDF_OUTLINE_FONT_SUBSET* subsetPtr : m_outlineFontManager->AllSubsets() )
1550 {
1551 if( subsetPtr && subsetPtr->FontHandle() >= 0 )
1552 fmt::println( m_outputFile, " {} {} 0 R", subsetPtr->ResourceName(), subsetPtr->FontHandle() );
1553 }
1554 }
1555
1557 {
1558 for( PDF_STROKE_FONT_SUBSET* subsetPtr : m_strokeFontManager->AllSubsets() )
1559 {
1560 const PDF_STROKE_FONT_SUBSET& subset = *subsetPtr;
1561 if( subset.FontHandle() >= 0 )
1562 fmt::println( m_outputFile, " {} {} 0 R", subset.ResourceName(), subset.FontHandle() );
1563 }
1564 }
1565
1566 fmt::println( m_outputFile, ">>" );
1568
1569 // Named image dictionary (was allocated, now we emit it)
1571 fmt::println( m_outputFile, "<<\n" );
1572
1573 for( const auto& [imgHandle, image] : m_imageHandles )
1574 {
1575 fmt::print( m_outputFile, " /Im{} {} 0 R\n", imgHandle, imgHandle );
1576 }
1577
1578 fmt::println( m_outputFile, ">>" );
1580
1581 // Emit images with optional SMask for transparency
1582 for( const auto& [imgHandle, image] : m_imageHandles )
1583 {
1584 // Init wxFFile so wxFFileOutputStream won't close file in dtor.
1585 wxFFile outputFFile( m_outputFile );
1586
1587 // Image
1588 startPdfObject( imgHandle );
1589 int imgLenHandle = allocPdfObject();
1590 int smaskHandle = ( image.HasAlpha() || image.HasMask() ) ? allocPdfObject() : -1;
1591
1592 fmt::print( m_outputFile,
1593 "<<\n"
1594 "/Type /XObject\n"
1595 "/Subtype /Image\n"
1596 "/BitsPerComponent 8\n"
1597 "/ColorSpace {}\n"
1598 "/Width {}\n"
1599 "/Height {}\n"
1600 "/Filter /FlateDecode\n"
1601 "/Length {} 0 R\n", // Length is deferred
1602 m_colorMode ? "/DeviceRGB" : "/DeviceGray",
1603 image.GetWidth(),
1604 image.GetHeight(),
1605 imgLenHandle );
1606
1607 if( smaskHandle != -1 )
1608 fmt::println( m_outputFile, "/SMask {} 0 R", smaskHandle );
1609
1610 fmt::println( m_outputFile, ">>" );
1611 fmt::println( m_outputFile, "stream" );
1612
1613 long imgStreamStart = ftell( m_outputFile );
1614
1615 {
1616 wxFFileOutputStream ffos( outputFFile );
1617 wxZlibOutputStream zos( ffos, wxZ_BEST_COMPRESSION, wxZLIB_ZLIB );
1618 wxDataOutputStream dos( zos );
1619
1620 WriteImageStream( image, dos, m_renderSettings->GetBackgroundColor().ToColour(),
1621 m_colorMode );
1622 }
1623
1624 long imgStreamSize = ftell( m_outputFile ) - imgStreamStart;
1625
1626 fmt::print( m_outputFile, "\nendstream\n" );
1628
1629 startPdfObject( imgLenHandle );
1630 fmt::println( m_outputFile, "{}", imgStreamSize );
1632
1633 if( smaskHandle != -1 )
1634 {
1635 // SMask
1636 startPdfObject( smaskHandle );
1637 int smaskLenHandle = allocPdfObject();
1638
1639 fmt::print( m_outputFile,
1640 "<<\n"
1641 "/Type /XObject\n"
1642 "/Subtype /Image\n"
1643 "/BitsPerComponent 8\n"
1644 "/ColorSpace /DeviceGray\n"
1645 "/Width {}\n"
1646 "/Height {}\n"
1647 "/Length {} 0 R\n"
1648 "/Filter /FlateDecode\n"
1649 ">>\n", // Length is deferred
1650 image.GetWidth(),
1651 image.GetHeight(),
1652 smaskLenHandle );
1653
1654 fmt::println( m_outputFile, "stream" );
1655
1656 long smaskStreamStart = ftell( m_outputFile );
1657
1658 {
1659 wxFFileOutputStream ffos( outputFFile );
1660 wxZlibOutputStream zos( ffos, wxZ_BEST_COMPRESSION, wxZLIB_ZLIB );
1661 wxDataOutputStream dos( zos );
1662
1664 }
1665
1666 long smaskStreamSize = ftell( m_outputFile ) - smaskStreamStart;
1667
1668 fmt::print( m_outputFile, "\nendstream\n" );
1670
1671 startPdfObject( smaskLenHandle );
1672 fmt::println( m_outputFile, "{}", (unsigned) smaskStreamSize );
1674 }
1675
1676 outputFFile.Detach(); // Don't close it
1677 }
1678
1679 for( const auto& [ linkHandle, linkPair ] : m_hyperlinkHandles )
1680 {
1681 BOX2D box = linkPair.first;
1682 wxString url = linkPair.second;
1683
1684 startPdfObject( linkHandle );
1685
1686 fmt::print( m_outputFile,
1687 "<<\n"
1688 "/Type /Annot\n"
1689 "/Subtype /Link\n"
1690 "/Rect [{} {} {} {}]\n"
1691 "/Border [16 16 0]\n",
1695 encodeDoubleForPlotter( box.GetTop() ) );
1696
1697 wxString pageNumber;
1698 bool pageFound = false;
1699
1700 if( EDA_TEXT::IsGotoPageHref( url, &pageNumber ) )
1701 {
1702 for( size_t ii = 0; ii < m_pageNumbers.size(); ++ii )
1703 {
1704 if( m_pageNumbers[ii] == pageNumber )
1705 {
1706 fmt::print( m_outputFile,
1707 "/Dest [{} 0 R /FitB]\n"
1708 ">>\n",
1709 m_pageHandles[ii] );
1710
1711 pageFound = true;
1712 break;
1713 }
1714 }
1715
1716 if( !pageFound )
1717 {
1718 // destination page is not being plotted, assign the NOP action to the link
1719 fmt::print( m_outputFile, "/A << /Type /Action /S /NOP >>\n"
1720 ">>\n" );
1721 }
1722 }
1723 else
1724 {
1725 if( m_project )
1726 url = ResolveUriByEnvVars( url, m_project );
1727
1728 fmt::print( m_outputFile,
1729 "/A << /Type /Action /S /URI /URI {} >>\n"
1730 ">>\n",
1731 encodeStringForPlotter( url ) );
1732 }
1733
1735 }
1736
1737 for( const auto& [ menuHandle, menuPair ] : m_hyperlinkMenuHandles )
1738 {
1739 const BOX2D& box = menuPair.first;
1740 const std::vector<wxString>& urls = menuPair.second;
1741 wxString js = wxT( "ShM([\n" );
1742
1743 for( const wxString& url : urls )
1744 {
1745 if( url.StartsWith( "!" ) )
1746 {
1747 wxString property = url.AfterFirst( '!' );
1748
1749 if( property.Find( "http:" ) >= 0 )
1750 {
1751 wxString href = property.substr( property.Find( "http:" ) );
1752
1753 if( m_project )
1754 href = ResolveUriByEnvVars( href, m_project );
1755
1756 js += wxString::Format( wxT( "[\"%s\", \"%s\"],\n" ),
1757 EscapeString( href, CTX_JS_STR ),
1758 EscapeString( href, CTX_JS_STR ) );
1759 }
1760 else if( property.Find( "https:" ) >= 0 )
1761 {
1762 wxString href = property.substr( property.Find( "https:" ) );
1763
1764 if( m_project )
1765 href = ResolveUriByEnvVars( href, m_project );
1766
1767 js += wxString::Format( wxT( "[\"%s\", \"%s\"],\n" ),
1768 EscapeString( href, CTX_JS_STR ),
1769 EscapeString( href, CTX_JS_STR ) );
1770 }
1771 else if( property.Find( "file:" ) >= 0 )
1772 {
1773 wxString href = property.substr( property.Find( "file:" ) );
1774
1775 if( m_project )
1776 href = ResolveUriByEnvVars( href, m_project );
1777
1778 href = NormalizeFileUri( href );
1779
1780 js += wxString::Format( wxT( "[\"%s\", \"%s\"],\n" ),
1781 EscapeString( href, CTX_JS_STR ),
1782 EscapeString( href, CTX_JS_STR ) );
1783 }
1784 else
1785 {
1786 js += wxString::Format( wxT( "[\"%s\"],\n" ),
1787 EscapeString( property, CTX_JS_STR ) );
1788 }
1789 }
1790 else if( url.StartsWith( "#" ) )
1791 {
1792 wxString pageNumber = url.AfterFirst( '#' );
1793
1794 for( size_t ii = 0; ii < m_pageNumbers.size(); ++ii )
1795 {
1796 if( m_pageNumbers[ii] == pageNumber )
1797 {
1798 wxString menuText = wxString::Format( _( "Show Page %s" ), pageNumber );
1799
1800 js += wxString::Format( wxT( "[\"%s\", \"#%d\"],\n" ),
1801 EscapeString( menuText, CTX_JS_STR ),
1802 static_cast<int>( ii ) );
1803 break;
1804 }
1805 }
1806 }
1807 else if( url.StartsWith( "http:" ) || url.StartsWith( "https:" )
1808 || url.StartsWith( "file:" ) )
1809 {
1810 wxString href = url;
1811
1812 if( m_project )
1813 href = ResolveUriByEnvVars( url, m_project );
1814
1815 if( url.StartsWith( "file:" ) )
1816 href = NormalizeFileUri( href );
1817
1818 wxString menuText = wxString::Format( _( "Open %s" ), href );
1819
1820 js += wxString::Format( wxT( "[\"%s\", \"%s\"],\n" ),
1821 EscapeString( href, CTX_JS_STR ),
1822 EscapeString( href, CTX_JS_STR ) );
1823 }
1824 }
1825
1826 js += wxT( "]);" );
1827
1828 startPdfObject( menuHandle );
1829
1830 fmt::print( m_outputFile,
1831 "<<\n"
1832 "/Type /Annot\n"
1833 "/Subtype /Link\n"
1834 "/Rect [{} {} {} {}]\n"
1835 "/Border [16 16 0]\n",
1839 encodeDoubleForPlotter( box.GetTop() ) );
1840
1841 fmt::print( m_outputFile,
1842 "/A << /Type /Action /S /JavaScript /JS {} >>\n"
1843 ">>\n",
1844 encodeStringForPlotter( js ) );
1845
1847 }
1848
1849 {
1851
1852 wxString js = R"JS(
1853function ShM(aEntries) {
1854 var aParams = [];
1855 for (var i in aEntries) {
1856 aParams.push({
1857 cName: aEntries[i][0],
1858 cReturn: aEntries[i][1]
1859 })
1860 }
1861
1862 var cChoice = app.popUpMenuEx.apply(app, aParams);
1863 if (cChoice != null && cChoice.substring(0, 1) == '#') this.pageNum = parseInt(cChoice.slice(1));
1864 else if (cChoice != null && cChoice.substring(0, 4) == 'http') app.launchURL(cChoice);
1865 else if (cChoice != null && cChoice.substring(0, 4) == 'file') app.openDoc(cChoice.substring(7));
1866}
1867)JS";
1868
1869 fmt::print( m_outputFile,
1870 "<< /JavaScript\n"
1871 " << /Names\n"
1872 " [ (JSInit) << /Type /Action /S /JavaScript /JS {} >> ]\n"
1873 " >>\n"
1874 ">>\n",
1875 encodeStringForPlotter( js ) );
1876
1878 }
1879}
1880
1882{
1883 // We can end up here if there was nothing to plot
1884 if( !m_outputFile )
1885 return false;
1886
1887 // Close the current page (often the only one)
1888 ClosePage();
1889
1890 if( !m_3dExportMode )
1891 {
1893 }
1894
1895 /* The page tree: it's a B-tree but luckily we only have few pages!
1896 So we use just an array... The handle was allocated at the beginning,
1897 now we instantiate the corresponding object */
1899 fmt::print( m_outputFile,
1900 "<<\n"
1901 "/Type /Pages\n"
1902 "/Kids [\n" );
1903
1904 for( unsigned i = 0; i < m_pageHandles.size(); i++ )
1905 fmt::println( m_outputFile, "{} 0 R", m_pageHandles[i] );
1906
1907 fmt::print( m_outputFile,
1908 "]\n"
1909 "/Count {}\n"
1910 ">>\n", m_pageHandles.size() );
1912
1913 int infoDictHandle = startPdfObject();
1914
1915 std::time_t time = std::time( nullptr );
1916 std::tm tm{};
1917 #if defined( _WIN32 ) || defined( _MSC_VER )
1918 localtime_s( &tm, &time );
1919 #else
1920 localtime_r( &time, &tm );
1921 #endif
1922 std::string dt = fmt::format( "D:{:%Y:%m:%d:%H:%M:%S}", tm );
1923
1924 if( m_title.IsEmpty() )
1925 {
1926 // Windows uses '\' and other platforms use '/' as separator
1927 m_title = m_filename.AfterLast( '\\' );
1928 m_title = m_title.AfterLast( '/' );
1929 }
1930
1931 fmt::print( m_outputFile,
1932 "<<\n"
1933 "/Producer (KiCad PDF)\n"
1934 "/CreationDate ({})\n"
1935 "/Creator {}\n"
1936 "/Title {}\n"
1937 "/Author {}\n"
1938 "/Subject {}\n",
1939 dt,
1944
1945 fmt::println( m_outputFile, ">>" );
1947
1948 // Let's dump in the outline
1949 int outlineHandle = -1;
1950 if( !m_3dExportMode )
1951 {
1952 outlineHandle = emitOutline();
1953 }
1954
1955 // The catalog, at last
1956 int catalogHandle = startPdfObject();
1957
1958 if( outlineHandle > 0 )
1959 {
1960 fmt::println( m_outputFile,
1961 "<<\n"
1962 "/Type /Catalog\n"
1963 "/Pages {} 0 R\n"
1964 "/Version /1.5\n"
1965 "/PageMode /UseOutlines\n"
1966 "/Outlines {} 0 R\n"
1967 "/Names {} 0 R\n"
1968 "/PageLayout /SinglePage\n"
1969 ">>",
1971 outlineHandle,
1973 }
1974 else
1975 {
1976 fmt::println( m_outputFile,
1977 "<<\n"
1978 "/Type /Catalog\n"
1979 "/Pages {} 0 R\n"
1980 "/Version /1.5\n"
1981 "/PageMode /UseNone\n"
1982 "/PageLayout /SinglePage\n"
1983 ">>",
1985 }
1986
1988
1989 /* Emit the xref table (format is crucial to the byte, each entry must
1990 be 20 bytes long, and object zero must be done in that way). Also
1991 the offset must be kept along for the trailer */
1992 long xref_start = ftell( m_outputFile );
1993 fmt::print( m_outputFile,
1994 "xref\n"
1995 "0 {}\n"
1996 "0000000000 65535 f \n",
1997 m_xrefTable.size() );
1998
1999 for( unsigned i = 1; i < m_xrefTable.size(); i++ )
2000 {
2001 fmt::print( m_outputFile, "{:010d} 00000 n \n", m_xrefTable[i] );
2002 }
2003
2004 // Done the xref, go for the trailer
2005 fmt::print( m_outputFile,
2006 "trailer\n"
2007 "<< /Size {} /Root {} 0 R /Info {} 0 R >>\n"
2008 "startxref\n"
2009 "{}\n" // The offset we saved before
2010 "%%EOF\n",
2011 m_xrefTable.size(),
2012 catalogHandle,
2013 infoDictHandle,
2014 xref_start );
2015
2016 fclose( m_outputFile );
2017 m_outputFile = nullptr;
2018
2019 return true;
2020}
2021
2022
2023void PDF_PLOTTER::Text( const VECTOR2I& aPos,
2024 const COLOR4D& aColor,
2025 const wxString& aText,
2026 const EDA_ANGLE& aOrient,
2027 const VECTOR2I& aSize,
2028 enum GR_TEXT_H_ALIGN_T aH_justify,
2029 enum GR_TEXT_V_ALIGN_T aV_justify,
2030 int aWidth,
2031 bool aItalic,
2032 bool aBold,
2033 bool aMultilineAllowed,
2034 KIFONT::FONT* aFont,
2035 const KIFONT::METRICS& aFontMetrics,
2036 void* aData )
2037{
2038 // PDF files do not like 0 sized texts which create broken files.
2039 if( aSize.x == 0 || aSize.y == 0 )
2040 return;
2041
2042 wxString text( aText );
2043
2044 if( text.Contains( wxS( "@{" ) ) )
2045 {
2046 EXPRESSION_EVALUATOR evaluator;
2047 text = evaluator.Evaluate( text );
2048 }
2049
2050 SetColor( aColor );
2051 SetCurrentLineWidth( aWidth, aData );
2052
2053 if( !aFont )
2054 aFont = KIFONT::FONT::GetFont( m_renderSettings->GetDefaultFont() );
2055
2056 VECTOR2I t_size( std::abs( aSize.x ), std::abs( aSize.y ) );
2057 bool textMirrored = aSize.x < 0;
2058
2059 // Parse the text for markup
2060 // IMPORTANT: Use explicit UTF-8 encoding. wxString::ToStdString() is locale-dependent
2061 // and under C/POSIX locale can drop or mangle non-ASCII, leading to missing CMaps.
2062 // The markup parser expects UTF-8 bytes.
2063 UTF8 utf8Text( text );
2064 MARKUP::MARKUP_PARSER markupParser( utf8Text.substr() );
2065 std::unique_ptr<MARKUP::NODE> markupTree( markupParser.Parse() );
2066
2067 if( !markupTree )
2068 {
2069 wxLogTrace( tracePdfPlotter, "PDF_PLOTTER::Text: Markup parsing failed, falling back to plain text." );
2070 // Fallback to simple text rendering if parsing fails
2071 wxStringTokenizer str_tok( text, " ", wxTOKEN_RET_DELIMS );
2072 VECTOR2I pos = aPos;
2073
2074 while( str_tok.HasMoreTokens() )
2075 {
2076 wxString word = str_tok.GetNextToken();
2077 pos = renderWord( word, pos, t_size, aOrient, textMirrored, aWidth, aBold, aItalic, aFont,
2078 aFontMetrics, aV_justify, 0 );
2079 }
2080 return;
2081 }
2082
2083 // Calculate the full text bounding box for alignment
2084 VECTOR2I full_box( aFont->StringBoundaryLimits( text, t_size, aWidth, aBold, aItalic, aFontMetrics ) );
2085
2086 if( textMirrored )
2087 full_box.x *= -1;
2088
2089 VECTOR2I box_x( full_box.x, 0 );
2090 VECTOR2I box_y( 0, full_box.y );
2091
2092 RotatePoint( box_x, aOrient );
2093 RotatePoint( box_y, aOrient );
2094
2095 VECTOR2I pos( aPos );
2096 if( aH_justify == GR_TEXT_H_ALIGN_CENTER )
2097 pos -= box_x / 2;
2098 else if( aH_justify == GR_TEXT_H_ALIGN_RIGHT )
2099 pos -= box_x;
2100
2101 if( aV_justify == GR_TEXT_V_ALIGN_CENTER )
2102 pos += box_y / 2;
2103 else if( aV_justify == GR_TEXT_V_ALIGN_TOP )
2104 pos += box_y;
2105
2106 // Render markup tree
2107 std::vector<OVERBAR_INFO> overbars;
2108 renderMarkupNode( markupTree.get(), pos, t_size, aOrient, textMirrored, aWidth,
2109 aBold, aItalic, aFont, aFontMetrics, aV_justify, 0, overbars );
2110
2111 // Draw any overbars that were accumulated
2112 drawOverbars( overbars, aOrient, aFontMetrics );
2113}
2114
2115
2116VECTOR2I PDF_PLOTTER::renderWord( const wxString& aWord, const VECTOR2I& aPosition,
2117 const VECTOR2I& aSize, const EDA_ANGLE& aOrient,
2118 bool aTextMirrored, int aWidth, bool aBold, bool aItalic,
2119 KIFONT::FONT* aFont, const KIFONT::METRICS& aFontMetrics,
2120 enum GR_TEXT_V_ALIGN_T aV_justify, TEXT_STYLE_FLAGS aTextStyle )
2121{
2122 if( wxGetEnv( "KICAD_DEBUG_SYN_STYLE", nullptr ) )
2123 {
2124 int styleFlags = 0;
2125 if( aFont->IsOutline() )
2126 styleFlags = static_cast<KIFONT::OUTLINE_FONT*>( aFont )->GetFace() ? static_cast<KIFONT::OUTLINE_FONT*>( aFont )->GetFace()->style_flags : 0;
2127 wxLogTrace( tracePdfPlotter, "renderWord enter word='%s' bold=%d italic=%d textStyle=%u styleFlags=%d", TO_UTF8( aWord ), (int) aBold, (int) aItalic, (unsigned) aTextStyle, styleFlags );
2128 }
2129
2130 // Don't try to output a blank string, but handle space characters for word separation
2131 if( aWord.empty() )
2132 return aPosition;
2133
2134 // If the word is just a space character, advance position by space width and continue
2135 if( aWord == wxT( " " ) )
2136 {
2137 // Calculate space width and advance position
2138 VECTOR2I spaceBox( aFont->StringBoundaryLimits( "n", aSize, aWidth, aBold, aItalic,
2139 aFontMetrics ).x / 2, 0 );
2140
2141 if( aTextMirrored )
2142 spaceBox.x *= -1;
2143
2144 VECTOR2I rotatedSpaceBox = spaceBox;
2145 RotatePoint( rotatedSpaceBox, aOrient );
2146 return aPosition + rotatedSpaceBox;
2147 }
2148
2149 // If the word contains tab characters, we need to handle them specially.
2150 // Split by tabs and render each segment, advancing to the next tab stop for each tab.
2151 if( aWord.Contains( wxT( '\t' ) ) )
2152 {
2153 constexpr double TAB_WIDTH = 4 * 0.6;
2154
2155 VECTOR2I pos = aPosition;
2156 wxString segment;
2157
2158 for( wxUniChar c : aWord )
2159 {
2160 if( c == '\t' )
2161 {
2162 if( !segment.IsEmpty() )
2163 {
2164 pos = renderWord( segment, pos, aSize, aOrient, aTextMirrored, aWidth, aBold,
2165 aItalic, aFont, aFontMetrics, aV_justify, aTextStyle );
2166 segment.clear();
2167 }
2168
2169 int tabWidth = KiROUND( aSize.x * TAB_WIDTH );
2170 int currentIntrusion = ( pos.x - aPosition.x ) % tabWidth;
2171 VECTOR2I tabAdvance( tabWidth - currentIntrusion, 0 );
2172
2173 if( aTextMirrored )
2174 tabAdvance.x *= -1;
2175
2176 RotatePoint( tabAdvance, aOrient );
2177 pos += tabAdvance;
2178 }
2179 else
2180 {
2181 segment += c;
2182 }
2183 }
2184
2185 if( !segment.IsEmpty() )
2186 {
2187 pos = renderWord( segment, pos, aSize, aOrient, aTextMirrored, aWidth, aBold, aItalic,
2188 aFont, aFontMetrics, aV_justify, aTextStyle );
2189 }
2190
2191 return pos;
2192 }
2193
2194 // Compute transformation parameters for this word
2195 double ctm_a, ctm_b, ctm_c, ctm_d, ctm_e, ctm_f;
2196 double wideningFactor, heightFactor;
2197
2198 computeTextParameters( aPosition, aWord, aOrient, aSize, aTextMirrored, GR_TEXT_H_ALIGN_LEFT,
2199 GR_TEXT_V_ALIGN_BOTTOM, aWidth, aItalic, aBold, &wideningFactor,
2200 &ctm_a, &ctm_b, &ctm_c, &ctm_d, &ctm_e, &ctm_f, &heightFactor );
2201
2202 // Calculate next position for word spacing
2203 VECTOR2I bbox( aFont->StringBoundaryLimits( aWord, aSize, aWidth, aBold, aItalic, aFontMetrics ).x, 0 );
2204
2205 if( aTextMirrored )
2206 bbox.x *= -1;
2207
2208 RotatePoint( bbox, aOrient );
2209 VECTOR2I nextPos = aPosition + bbox;
2210
2211 // Apply vertical offset for subscript/superscript
2212 // Stroke font positioning (baseline) already correct per user feedback.
2213 // Outline fonts need: superscript +1 full font height higher; subscript +1 full font height higher
2214 if( aTextStyle & TEXT_STYLE::SUPERSCRIPT )
2215 {
2216 double factor = aFont->IsOutline() ? 0.050 : 0.030; // stroke original ~0.40, outline needs +1.0
2217 VECTOR2I offset( 0, static_cast<int>( std::lround( aSize.y * factor ) ) );
2218 RotatePoint( offset, aOrient );
2219 ctm_e -= offset.x;
2220 ctm_f += offset.y; // Note: PDF Y increases upward
2221 }
2222 else if( aTextStyle & TEXT_STYLE::SUBSCRIPT )
2223 {
2224 // For outline fonts raise by one font height versus stroke (which shifts downward slightly)
2225 VECTOR2I offset( 0, 0 );
2226
2227 if( aFont->IsStroke() )
2228 offset.y = static_cast<int>( std::lround( aSize.y * 0.01 ) );
2229
2230 RotatePoint( offset, aOrient );
2231 ctm_e += offset.x;
2232 ctm_f -= offset.y; // Note: PDF Y increases upward
2233 }
2234
2235 // Render the word using existing outline font logic
2236 if( aFont->IsOutline() )
2237 {
2238 std::vector<PDF_OUTLINE_FONT_RUN> outlineRuns;
2239
2241 {
2242 m_outlineFontManager->EncodeString( aWord, static_cast<KIFONT::OUTLINE_FONT*>( aFont ),
2243 ( aItalic || ( aTextStyle & TEXT_STYLE::ITALIC ) ),
2244 ( aBold || ( aTextStyle & TEXT_STYLE::BOLD ) ),
2245 &outlineRuns );
2246 }
2247
2248 if( !outlineRuns.empty() )
2249 {
2250 // Apply baseline adjustment (keeping existing logic)
2251 double baseline_factor = 0.17;
2252 double alignment_multiplier = 1.0;
2253
2254 if( aV_justify == GR_TEXT_V_ALIGN_CENTER )
2255 alignment_multiplier = 2.0;
2256 else if( aV_justify == GR_TEXT_V_ALIGN_TOP )
2257 alignment_multiplier = 4.0;
2258
2259 VECTOR2D font_size_dev = userToDeviceSize( aSize );
2260 double baseline_adjustment = font_size_dev.y * baseline_factor * alignment_multiplier;
2261
2262 double adjusted_ctm_e = ctm_e;
2263 double adjusted_ctm_f = ctm_f;
2264
2265 double angle_rad = aOrient.AsRadians();
2266 double cos_angle = cos( angle_rad );
2267 double sin_angle = sin( angle_rad );
2268
2269 adjusted_ctm_e = ctm_e - baseline_adjustment * sin_angle;
2270 adjusted_ctm_f = ctm_f + baseline_adjustment * cos_angle;
2271
2272 double adj_c = ctm_c;
2273 double adj_d = ctm_d;
2274
2275 // Synthetic italic (shear) for outline font if requested but font not intrinsically italic
2276 bool syntheticItalicApplied = false;
2277 double appliedTilt = 0.0;
2278 double syn_c = adj_c;
2279 double syn_d = adj_d;
2280 double syn_a = ctm_a;
2281 double syn_b = ctm_b;
2282 bool wantItalic = ( aItalic || ( aTextStyle & TEXT_STYLE::ITALIC ) );
2283
2284 if( std::getenv( "KICAD_FORCE_SYN_ITALIC" ) )
2285 wantItalic = true; // debug: ensure path triggers when forcing synthetic italic
2286
2287 bool wantBold = ( aBold || ( aTextStyle & TEXT_STYLE::BOLD ) );
2288 bool fontIsItalic = aFont->IsItalic();
2289 bool fontIsBold = aFont->IsBold();
2290 bool fontIsFakeItalic = static_cast<KIFONT::OUTLINE_FONT*>( aFont )->IsFakeItalic();
2291 bool fontIsFakeBold = static_cast<KIFONT::OUTLINE_FONT*>( aFont )->IsFakeBold();
2292
2293 // Environment overrides for testing synthetic italics:
2294 // KICAD_FORCE_SYN_ITALIC=1 forces synthetic shear even if font has italic face
2295 // KICAD_SYN_ITALIC_TILT=<float degrees or tangent?>: if value contains 'deg' treat as degrees,
2296 // otherwise treat as raw tilt factor (x += tilt*y)
2297 bool forceSynItalic = false;
2298 double overrideTilt = 0.0;
2299
2300 if( const char* envForce = std::getenv( "KICAD_FORCE_SYN_ITALIC" ) )
2301 {
2302 if( *envForce != '\0' && *envForce != '0' )
2303 forceSynItalic = true;
2304 }
2305
2306 if( const char* envTilt = std::getenv( "KICAD_SYN_ITALIC_TILT" ) )
2307 {
2308 std::string tiltStr( envTilt );
2309
2310 try
2311 {
2312 if( tiltStr.find( "deg" ) != std::string::npos )
2313 {
2314 double deg = std::stod( tiltStr );
2315 overrideTilt = tan( deg * M_PI / 180.0 );
2316 }
2317 else
2318 {
2319 overrideTilt = std::stod( tiltStr );
2320 }
2321 }
2322 catch( ... )
2323 {
2324 overrideTilt = 0.0; // ignore malformed
2325 }
2326 }
2327
2328 // Trace after we know style flags
2329 wxLogTrace( tracePdfPlotter,
2330 "Outline path word='%s' runs=%zu wantItalic=%d fontIsItalic=%d fontIsFakeItalic=%d wantBold=%d fontIsBold=%d fontIsFakeBold=%d forceSyn=%d",
2331 TO_UTF8( aWord ), outlineRuns.size(), (int) wantItalic, (int) fontIsItalic,
2332 (int) fontIsFakeItalic, (int) wantBold, (int) fontIsBold, (int) fontIsFakeBold,
2333 (int) forceSynItalic );
2334
2335 // Apply synthetic italic if:
2336 // - Italic requested AND outline font
2337 // - And either forceSynItalic env var set OR there is no REAL italic face.
2338 // (A fake italic flag from fontconfig substitution should NOT block synthetic shear.)
2339 bool realItalicFace = fontIsItalic && !fontIsFakeItalic;
2340
2341 if( wantItalic && ( forceSynItalic || !realItalicFace ) )
2342 {
2343 // We want to apply a horizontal shear so that x' = x + tilt * y in the glyph's
2344 // local coordinate system BEFORE rotation. The existing text matrix columns are:
2345 // first column = (a, b)^T -> x-axis direction & scale
2346 // second column = (c, d)^T -> y-axis direction & scale
2347 // Prepending a shear matrix S = [[1 tilt][0 1]] (i.e. T' = T * S is WRONG here).
2348 // We need to LEFT-multiply: T' = R * S where R is the original rotation/scale.
2349 // Left multiplication keeps first column unchanged and adds (tilt * firstColumn)
2350 // to the second column: (c', d') = (c + tilt * a, d + tilt * b).
2351 // This produces a right-leaning italic for positive tilt.
2352 double tilt = ( overrideTilt != 0.0 ) ? overrideTilt : ITALIC_TILT;
2353
2354 if( wideningFactor < 0 ) // mirrored text should mirror the shear
2355 tilt = -tilt;
2356
2357 syn_c = adj_c + tilt * syn_a;
2358 syn_d = adj_d + tilt * syn_b;
2359 appliedTilt = tilt;
2360 syntheticItalicApplied = true;
2361
2362 wxLogTrace( tracePdfPlotter, "Synthetic italic shear applied: tilt=%f a=%f b=%f c->%f d->%f",
2363 tilt, syn_a, syn_b, syn_c, syn_d );
2364 }
2365
2366 if( wantBold && !fontIsBold )
2367 {
2368 // Slight horizontal widening to simulate bold (~3%)
2369 syn_a *= 1.03;
2370 syn_b *= 1.03;
2371 }
2372
2373 if( syntheticItalicApplied )
2374 {
2375 // PDF comment to allow manual inspection in the output stream
2376 fmt::print( m_workFile, "% syn-italic tilt={} a={} b={} c={} d={}\n",
2377 appliedTilt, syn_a, syn_b, syn_c, syn_d );
2378 }
2379
2380 fmt::print( m_workFile, "q {:f} {:f} {:f} {:f} {:f} {:f} cm BT {} Tr {} Tz ",
2381 syn_a, syn_b, syn_c, syn_d, adjusted_ctm_e, adjusted_ctm_f,
2382 0, // render_mode
2383 encodeDoubleForPlotter( wideningFactor * 100 ) );
2384
2385 for( const PDF_OUTLINE_FONT_RUN& run : outlineRuns )
2386 {
2387 fmt::print( m_workFile, "{} {} Tf <",
2388 run.m_subset->ResourceName(), encodeDoubleForPlotter( heightFactor ) );
2389
2390 for( const PDF_OUTLINE_FONT_GLYPH& glyph : run.m_glyphs )
2391 {
2392 fmt::print( m_workFile, "{:02X}{:02X}",
2393 static_cast<unsigned char>( ( glyph.cid >> 8 ) & 0xFF ),
2394 static_cast<unsigned char>( glyph.cid & 0xFF ) );
2395 }
2396
2397 fmt::print( m_workFile, "> Tj " );
2398 }
2399
2400 fmt::println( m_workFile, "ET" );
2401 fmt::println( m_workFile, "Q" );
2402 }
2403 }
2404 else
2405 {
2406 // Handle stroke fonts
2407 if( !m_strokeFontManager )
2408 return nextPos;
2409
2410 wxLogTrace( tracePdfPlotter, "Stroke path word='%s' wantItalic=%d aItalic=%d aBold=%d",
2411 TO_UTF8( aWord ), (int) ( aItalic || ( aTextStyle & TEXT_STYLE::ITALIC ) ), (int) aItalic, (int) aBold );
2412
2413 std::vector<PDF_STROKE_FONT_RUN> runs;
2414 m_strokeFontManager->EncodeString( aWord, &runs, aBold, aItalic );
2415
2416 if( !runs.empty() )
2417 {
2418 VECTOR2D dev_size = userToDeviceSize( aSize );
2419 double fontSize = dev_size.y;
2420
2421 double adj_c = ctm_c;
2422 double adj_d = ctm_d;
2423
2424 if( aItalic )
2425 {
2426 double tilt = -ITALIC_TILT;
2427
2428 if( wideningFactor < 0 )
2429 tilt = -tilt;
2430
2431 adj_c -= ctm_a * tilt;
2432 adj_d -= ctm_b * tilt;
2433 }
2434
2435 fmt::print( m_workFile, "q {:f} {:f} {:f} {:f} {:f} {:f} cm BT {} Tr {} Tz ",
2436 ctm_a, ctm_b, adj_c, adj_d, ctm_e, ctm_f,
2437 0, // render_mode
2438 encodeDoubleForPlotter( wideningFactor * 100 ) );
2439
2440 for( const PDF_STROKE_FONT_RUN& run : runs )
2441 {
2442 fmt::print( m_workFile, "{} {} Tf {} Tj ",
2443 run.m_subset->ResourceName(),
2444 encodeDoubleForPlotter( fontSize ),
2445 encodeByteString( run.m_bytes ) );
2446 }
2447
2448 fmt::println( m_workFile, "ET" );
2449 fmt::println( m_workFile, "Q" );
2450 }
2451 }
2452
2453 return nextPos;
2454}
2455
2456
2458 const VECTOR2I& aBaseSize, const EDA_ANGLE& aOrient,
2459 bool aTextMirrored, int aWidth, bool aBaseBold, bool aBaseItalic,
2460 KIFONT::FONT* aFont, const KIFONT::METRICS& aFontMetrics,
2461 enum GR_TEXT_V_ALIGN_T aV_justify, TEXT_STYLE_FLAGS aTextStyle,
2462 std::vector<OVERBAR_INFO>& aOverbars )
2463{
2464 VECTOR2I nextPosition = aPosition;
2465
2466 if( !aNode )
2467 return nextPosition;
2468
2469 TEXT_STYLE_FLAGS currentStyle = aTextStyle;
2470 VECTOR2I currentSize = aBaseSize;
2471 bool drawOverbar = false;
2472
2473 // Handle markup node types
2474 if( !aNode->is_root() )
2475 {
2476 if( aNode->isSubscript() )
2477 {
2478 currentStyle |= TEXT_STYLE::SUBSCRIPT;
2479 // Subscript: smaller size and lower position
2480 currentSize = VECTOR2I( aBaseSize.x * 0.5, aBaseSize.y * 0.6 );
2481 }
2482 else if( aNode->isSuperscript() )
2483 {
2484 currentStyle |= TEXT_STYLE::SUPERSCRIPT;
2485 // Superscript: smaller size and higher position
2486 currentSize = VECTOR2I( aBaseSize.x * 0.5, aBaseSize.y * 0.6 );
2487 }
2488
2489 if( aNode->isOverbar() )
2490 {
2491 drawOverbar = true;
2492 // Overbar doesn't change font size, just adds decoration
2493 }
2494
2495 // Render content of this node if it has text
2496 if( aNode->has_content() )
2497 {
2498 wxString nodeText = aNode->asWxString();
2499
2500 // Process text content (simplified version of the main text processing)
2501 wxStringTokenizer str_tok( nodeText, " ", wxTOKEN_RET_DELIMS );
2502
2503 while( str_tok.HasMoreTokens() )
2504 {
2505 wxString word = str_tok.GetNextToken();
2506 nextPosition = renderWord( word, nextPosition, currentSize, aOrient, aTextMirrored,
2507 aWidth, aBaseBold || (currentStyle & TEXT_STYLE::BOLD),
2508 aBaseItalic || (currentStyle & TEXT_STYLE::ITALIC),
2509 aFont, aFontMetrics, aV_justify, currentStyle );
2510 }
2511 }
2512 }
2513
2514 // Process child nodes recursively
2515 for( const std::unique_ptr<MARKUP::NODE>& child : aNode->children )
2516 {
2517 VECTOR2I startPos = nextPosition;
2518
2519 nextPosition = renderMarkupNode( child.get(), nextPosition, currentSize, aOrient,
2520 aTextMirrored, aWidth, aBaseBold, aBaseItalic,
2521 aFont, aFontMetrics, aV_justify, currentStyle, aOverbars );
2522
2523 // Store overbar info for later rendering
2524 if( drawOverbar )
2525 {
2526 VECTOR2I endPos = nextPosition;
2527 aOverbars.push_back( { startPos, endPos, currentSize, aFont->IsOutline(), aV_justify } );
2528 }
2529 }
2530
2531 return nextPosition;
2532}
2533
2534
2535void PDF_PLOTTER::drawOverbars( const std::vector<OVERBAR_INFO>& aOverbars, const EDA_ANGLE& aOrient,
2536 const KIFONT::METRICS& aFontMetrics )
2537{
2538 for( const OVERBAR_INFO& overbar : aOverbars )
2539 {
2540 // Baseline direction (vector from start to end). If zero length, derive from orientation.
2541 VECTOR2D dir( overbar.endPos.x - overbar.startPos.x, overbar.endPos.y - overbar.startPos.y );
2542
2543 double len = hypot( dir.x, dir.y );
2544 if( len <= 1e-6 )
2545 {
2546 // Fallback: derive direction from orientation angle
2547 double ang = aOrient.AsRadians();
2548 dir.x = cos( ang );
2549 dir.y = sin( ang );
2550 len = 1.0;
2551 }
2552
2553 dir.x /= len;
2554 dir.y /= len;
2555
2556 // Perpendicular (rotate dir 90° CCW). Upward in text space so overbar sits above baseline.
2557 VECTOR2D nrm( -dir.y, dir.x );
2558
2559 // Base vertical offset distance in device units (baseline -> default overbar position)
2560 double barOffset = aFontMetrics.GetOverbarVerticalPosition( overbar.fontSize.y );
2561
2562 // Adjust further to match screen drawing. This is somewhat disturbing, but I can't figure
2563 // out why it's needed.
2564 if( overbar.isOutline )
2565 barOffset += overbar.fontSize.y * 0.16;
2566 else
2567 barOffset += overbar.fontSize.y * 0.32;
2568
2569 // Mirror the text vertical alignment adjustments used for baseline shifting.
2570 // Earlier logic scales baseline adjustment: CENTER ~2x, TOP ~4x. We apply proportional
2571 // extra raise so that overbars track visually with perceived baseline shift.
2572 double alignMult = 1.0;
2573
2574 switch( overbar.vAlign )
2575 {
2576 case GR_TEXT_V_ALIGN_CENTER: alignMult = overbar.isOutline ? 2.0 : 1.0; break;
2577 case GR_TEXT_V_ALIGN_TOP: alignMult = overbar.isOutline ? 4.0 : 1.0; break;
2578 default: alignMult = 1.0; break; // bottom
2579 }
2580
2581 if( alignMult > 1.0 )
2582 {
2583 // Scale only the baseline component (approx 17% of height, matching earlier baseline_factor)
2584 double baseline_factor = 0.17;
2585 barOffset += ( alignMult - 1.0 ) * ( baseline_factor * overbar.fontSize.y );
2586 }
2587
2588 // Trim to avoid rounded cap extension (assumes stroke caps); proportion of font width.
2589 double barTrim = overbar.fontSize.x * 0.1;
2590
2591 // Apply trim along baseline direction and offset along normal
2592 VECTOR2D startPt( overbar.startPos.x, overbar.startPos.y );
2593 VECTOR2D endPt( overbar.endPos.x, overbar.endPos.y );
2594
2595 // Both endpoints should share identical vertical (normal) offset above baseline.
2596 // Use a single offset vector offVec = -barOffset * nrm (negative because nrm points 'up').
2597 VECTOR2D offVec( -barOffset * nrm.x, -barOffset * nrm.y );
2598
2599 startPt.x += dir.x * barTrim + offVec.x;
2600 startPt.y += dir.y * barTrim + offVec.y;
2601 endPt.x -= dir.x * barTrim - offVec.x; // subtract trim, then apply same vertical offset
2602 endPt.y -= dir.y * barTrim - offVec.y;
2603
2604 VECTOR2I iStart = KiROUND( startPt.x, startPt.y );
2605 VECTOR2I iEnd = KiROUND( endPt.x, endPt.y );
2606
2607 MoveTo( iStart );
2608 LineTo( iEnd );
2609 PenFinish();
2610 }
2611}
2612
2613
2615 const COLOR4D& aColor,
2616 const wxString& aText,
2617 const TEXT_ATTRIBUTES& aAttributes,
2618 KIFONT::FONT* aFont,
2619 const KIFONT::METRICS& aFontMetrics,
2620 void* aData )
2621{
2622 VECTOR2I size = aAttributes.m_Size;
2623
2624 // PDF files do not like 0 sized texts which create broken files.
2625 if( size.x == 0 || size.y == 0 )
2626 return;
2627
2628 if( aAttributes.m_Mirrored )
2629 size.x = -size.x;
2630
2631 PDF_PLOTTER::Text( aPos, aColor, aText, aAttributes.m_Angle, size, aAttributes.m_Halign,
2632 aAttributes.m_Valign, aAttributes.m_StrokeWidth, aAttributes.m_Italic,
2633 aAttributes.m_Bold, aAttributes.m_Multiline, aFont, aFontMetrics, aData );
2634}
2635
2636
2637void PDF_PLOTTER::HyperlinkBox( const BOX2I& aBox, const wxString& aDestinationURL )
2638{
2639 m_hyperlinksInPage.push_back( std::make_pair( aBox, aDestinationURL ) );
2640}
2641
2642
2643void PDF_PLOTTER::HyperlinkMenu( const BOX2I& aBox, const std::vector<wxString>& aDestURLs )
2644{
2645 m_hyperlinkMenusInPage.push_back( std::make_pair( aBox, aDestURLs ) );
2646}
2647
2648
2649void PDF_PLOTTER::Bookmark( const BOX2I& aLocation, const wxString& aSymbolReference,
2650 const wxString &aGroupName )
2651{
2652
2653 m_bookmarksInPage[aGroupName].push_back( std::make_pair( aLocation, aSymbolReference ) );
2654}
2655
2656
2657void PDF_PLOTTER::Plot3DModel( const wxString& aSourcePath,
2658 const std::vector<PDF_3D_VIEW>& a3DViews )
2659{
2660 std::map<float, int> m_fovMap;
2661 std::vector<int> m_viewHandles;
2662
2663 for( const PDF_3D_VIEW& view : a3DViews )
2664 {
2665 // this is a strict need
2666 wxASSERT( view.m_cameraMatrix.size() == 12 );
2667
2668 int fovHandle = -1;
2669 if( !m_fovMap.contains( view.m_fov ) )
2670 {
2671 fovHandle = allocPdfObject();
2672 m_fovMap[view.m_fov] = fovHandle;
2673
2674 startPdfObject( fovHandle );
2675 fmt::print( m_outputFile,
2676 "<<\n"
2677 "/FOV {}\n"
2678 "/PS /Min\n"
2679 "/Subtype /P\n"
2680 ">>\n",
2681 encodeDoubleForPlotter( view.m_fov ) );
2683 }
2684 else
2685 {
2686 fovHandle = m_fovMap[view.m_fov];
2687 }
2688
2689 int viewHandle = allocPdfObject();
2690 startPdfObject( viewHandle );
2691
2692 fmt::print( m_outputFile,
2693 "<<\n"
2694 "/Type /3DView\n"
2695 "/XN ({})\n"
2696 "/IN ({})\n"
2697 "/MS /M\n"
2698 "/C2W [{:f} {:f} {:f} {:f} {:f} {:f} {:f} {:f} {:f} {:f} {:f} {:f}]\n"
2699 "/CO {:f}\n"
2700 "/NR false\n"
2701 "/BG<<\n"
2702 "/Type /3DBG\n"
2703 "/Subtype /SC\n"
2704 "/CS /DeviceRGB\n"
2705 "/C [1.000000 1.000000 1.000000]>>\n"
2706 "/P {} 0 R\n"
2707 "/LS<<\n"
2708 "/Type /3DLightingScheme\n"
2709 "/Subtype /CAD>>\n"
2710 ">>\n",
2711 view.m_name, view.m_name, view.m_cameraMatrix[0],
2712 view.m_cameraMatrix[1],
2713 view.m_cameraMatrix[2], view.m_cameraMatrix[3], view.m_cameraMatrix[4],
2714 view.m_cameraMatrix[5], view.m_cameraMatrix[6], view.m_cameraMatrix[7],
2715 view.m_cameraMatrix[8], view.m_cameraMatrix[9], view.m_cameraMatrix[10],
2716 view.m_cameraMatrix[11],
2717 view.m_cameraCenter,
2718 fovHandle );
2719
2721
2722 m_viewHandles.push_back( viewHandle );
2723 }
2724
2726
2727 // so we can get remotely stuff the length afterwards
2728 int modelLenHandle = allocPdfObject();
2729
2730 fmt::print( m_outputFile,
2731 "<<\n"
2732 "/Type /3D\n"
2733 "/Subtype /U3D\n"
2734 "/DV 0\n" );
2735
2736 fmt::print( m_outputFile, "/VA [" );
2737 for( int viewHandle : m_viewHandles )
2738 {
2739 fmt::print( m_outputFile, "{} 0 R ", viewHandle );
2740 }
2741 fmt::print( m_outputFile, "]\n" );
2742
2743 fmt::print( m_outputFile,
2744 "/Length {} 0 R\n"
2745 "/Filter /FlateDecode\n"
2746 ">>\n", // Length is deferred
2747 modelLenHandle );
2748
2749 fmt::println( m_outputFile, "stream" );
2750
2751 wxFFile outputFFile( m_outputFile );
2752
2753 fflush( m_outputFile );
2754 long imgStreamStart = ftell( m_outputFile );
2755
2756 size_t model_stored_size = 0;
2757 {
2758 wxFFileOutputStream ffos( outputFFile );
2759 wxZlibOutputStream zos( ffos, wxZ_BEST_COMPRESSION, wxZLIB_ZLIB );
2760
2761 wxFFileInputStream fileStream( aSourcePath );
2762 if( !fileStream.IsOk() )
2763 {
2764 wxLogError( _( "Failed to open 3D model file: %s" ), aSourcePath );
2765 }
2766
2767 zos.Write( fileStream );
2768 }
2769 fflush( m_outputFile );
2770 model_stored_size = ftell( m_outputFile );
2771 model_stored_size -= imgStreamStart; // Get the size of the compressed stream
2772
2773 fmt::print( m_outputFile, "\nendstream\n" );
2775
2776 startPdfObject( modelLenHandle );
2777 fmt::println( m_outputFile, "{}", (unsigned) model_stored_size );
2779
2780 outputFFile.Detach(); // Don't close it
2781}
2782
2783
2784std::vector<float> PDF_PLOTTER::CreateC2WMatrixFromAngles( const VECTOR3D& aTargetPosition,
2785 float aCameraDistance,
2786 float aYawDegrees,
2787 float aPitchDegrees,
2788 float aRollDegrees )
2789{
2790 float yRadians = glm::radians( aYawDegrees );
2791 float xRadians = glm::radians( aPitchDegrees );
2792 float zRadians = glm::radians( aRollDegrees );
2793
2794 // Create rotation matrix from Euler angles
2795 glm::mat4 rotationMatrix = glm::eulerAngleYXZ( yRadians, xRadians, zRadians );
2796
2797 // Calculate camera position based on target, distance, and rotation
2798 // Start with a vector pointing backward along the z-axis
2799 glm::vec4 cameraOffset = glm::vec4( 0.0f, 0.0f, aCameraDistance, 1.0f );
2800
2801 // Apply rotation to this offset
2802 cameraOffset = rotationMatrix * cameraOffset;
2803
2804 // Camera position is target position minus the rotated offset
2805 glm::vec3 cameraPosition = glm::vec3(aTargetPosition.x, aTargetPosition.y, aTargetPosition.z)
2806 - glm::vec3( cameraOffset );
2807
2808 std::vector<float> result( 12 );
2809
2810 // Handle rotation part in column-major order (first 9 elements)
2811 int index = 0;
2812 for( int col = 0; col < 3; ++col )
2813 {
2814 for( int row = 0; row < 3; ++row )
2815 {
2816 result[index++] = static_cast<float>( rotationMatrix[col][row] );
2817 }
2818 }
2819
2820 // Handle translation part (last 3 elements)
2821 result[9] = static_cast<float>( cameraPosition.x );
2822 result[10] = static_cast<float>( cameraPosition.y );
2823 result[11] = static_cast<float>( cameraPosition.z );
2824
2825 return result;
2826}
int index
void WriteImageSMaskStream(const wxImage &aImage, wxDataOutputStream &aOut)
void WriteImageStream(const wxImage &aImage, wxDataOutputStream &aOut, wxColor bg, bool colorMode)
BOX2< VECTOR2I > BOX2I
Definition box2.h:922
constexpr BOX2I KiROUND(const BOX2D &aBoxD)
Definition box2.h:990
BOX2< VECTOR2D > BOX2D
Definition box2.h:923
static const ADVANCED_CFG & GetCfg()
Get the singleton instance's config, which is shared by all consumers.
constexpr const Vec & GetPosition() const
Definition box2.h:211
constexpr const Vec GetEnd() const
Definition box2.h:212
constexpr void SetOrigin(const Vec &pos)
Definition box2.h:237
constexpr BOX2< Vec > & Normalize()
Ensure that the height and width are positive.
Definition box2.h:146
constexpr coord_type GetLeft() const
Definition box2.h:228
constexpr coord_type GetRight() const
Definition box2.h:217
constexpr void SetEnd(coord_type x, coord_type y)
Definition box2.h:297
constexpr coord_type GetTop() const
Definition box2.h:229
constexpr coord_type GetBottom() const
Definition box2.h:222
double AsRadians() const
Definition eda_angle.h:120
static bool IsGotoPageHref(const wxString &aHref, wxString *aDestination=nullptr)
Check if aHref is a valid internal hyperlink.
High-level wrapper for evaluating mathematical and string expressions in wxString format.
wxString Evaluate(const wxString &aInput)
Main evaluation function - processes input string and evaluates all} expressions.
FONT is an abstract base class for both outline and stroke fonts.
Definition font.h:131
static FONT * GetFont(const wxString &aFontName=wxEmptyString, bool aBold=false, bool aItalic=false, const std::vector< wxString > *aEmbeddedFiles=nullptr, bool aForDrawingSheet=false)
Definition font.cpp:147
virtual bool IsStroke() const
Definition font.h:138
virtual bool IsItalic() const
Definition font.h:141
virtual bool IsBold() const
Definition font.h:140
virtual bool IsOutline() const
Definition font.h:139
VECTOR2I StringBoundaryLimits(const wxString &aText, const VECTOR2I &aSize, int aThickness, bool aBold, bool aItalic, const METRICS &aFontMetrics) const
Compute the boundary limits of aText (the bounding box of all shapes).
Definition font.cpp:451
double GetOverbarVerticalPosition(double aGlyphHeight) const
Compute the vertical position of an overbar.
Definition font.h:100
Class OUTLINE_FONT implements outline font drawing.
A color representation with 4 components: red, green, blue, alpha.
Definition color4d.h:105
std::unique_ptr< NODE > Parse()
std::vector< int > m_pageHandles
Handles to the page objects.
FILE * m_workFile
Temporary file to construct the stream before zipping.
void emitOutlineFonts()
wxString m_parentPageName
virtual void ClosePage()
Close the current page in the PDF document (and emit its compressed stream).
void emitOutlineNode(OUTLINE_NODE *aNode, int aParentHandle, int aNextNode, int aPrevNode)
Emits a outline item object and recurses into any children.
std::map< int, wxImage > m_imageHandles
int emitOutline()
Starts emitting the outline object.
virtual bool EndPlot() override
int startPdfObject(int aHandle=-1)
Open a new PDF object and returns the handle if the parameter is -1.
virtual void PlotPoly(const std::vector< VECTOR2I > &aCornerList, FILL_T aFill, int aWidth=USE_DEFAULT_LINE_WIDTH, void *aData=nullptr) override
Polygon plotting for PDF.
virtual ~PDF_PLOTTER()
virtual void Circle(const VECTOR2I &pos, int diametre, FILL_T fill, int width) override
Circle drawing for PDF.
virtual void SetCurrentLineWidth(int width, void *aData=nullptr) override
Pen width setting for PDF.
int m_streamLengthHandle
Handle to the deferred stream length.
void PlotImage(const wxImage &aImage, const VECTOR2I &aPos, double aScaleFactor) override
PDF images are handles as inline, not XObject streams...
int m_jsNamesHandle
Handle for Names dictionary with JS.
wxString m_pageName
virtual void SetDash(int aLineWidth, LINE_STYLE aLineStyle) override
PDF supports dashed lines.
void HyperlinkMenu(const BOX2I &aBox, const std::vector< wxString > &aDestURLs) override
Create a clickable hyperlink menu with a rectangular click area.
virtual bool OpenFile(const wxString &aFullFilename) override
Open or create the plot file aFullFilename.
std::string encodeDoubleForPlotter(double aValue) const
Convert a double to a PDF-compatible numeric token (no exponent notation).
int m_fontResDictHandle
Font resource dictionary.
virtual void emitSetRGBColor(double r, double g, double b, double a) override
PDF supports colors fully.
void emitStrokeFonts()
std::map< int, std::pair< BOX2D, wxString > > m_hyperlinkHandles
Handles for all the hyperlink objects that will be deferred.
int m_pageTreeHandle
Handle to the root of the page tree object.
virtual void Text(const VECTOR2I &aPos, const COLOR4D &aColor, const wxString &aText, const EDA_ANGLE &aOrient, const VECTOR2I &aSize, enum GR_TEXT_H_ALIGN_T aH_justify, enum GR_TEXT_V_ALIGN_T aV_justify, int aWidth, bool aItalic, bool aBold, bool aMultilineAllowed, KIFONT::FONT *aFont, const KIFONT::METRICS &aFontMetrics, void *aData=nullptr) override
Draw text with the plotter.
int emitGoToAction(int aPageHandle, const VECTOR2I &aBottomLeft, const VECTOR2I &aTopRight)
Emit an action object that instructs a goto coordinates on a page.
void closePdfStream()
Finish the current PDF stream (writes the deferred length, too).
void Bookmark(const BOX2I &aBox, const wxString &aName, const wxString &aGroupName=wxEmptyString) override
Create a bookmark to a symbol.
std::vector< long > m_xrefTable
The PDF xref offset table.
void endPlotEmitResources()
virtual void Rect(const VECTOR2I &p1, const VECTOR2I &p2, FILL_T fill, int width, int aCornerRadius=0) override
Rectangles in PDF.
int startPdfStream(int aHandle=-1)
Start a PDF stream (for the page).
VECTOR2I renderMarkupNode(const MARKUP::NODE *aNode, const VECTOR2I &aPosition, const VECTOR2I &aBaseSize, const EDA_ANGLE &aOrient, bool aTextMirrored, int aWidth, bool aBaseBold, bool aBaseItalic, KIFONT::FONT *aFont, const KIFONT::METRICS &aFontMetrics, enum GR_TEXT_V_ALIGN_T aV_justify, TEXT_STYLE_FLAGS aTextStyle, std::vector< OVERBAR_INFO > &aOverbars)
Recursively render markup nodes with appropriate styling.
virtual void Arc(const VECTOR2D &aCenter, const EDA_ANGLE &aStartAngle, const EDA_ANGLE &aAngle, double aRadius, FILL_T aFill, int aWidth) override
The PDF engine can't directly plot arcs so we use polygonization.
void HyperlinkBox(const BOX2I &aBox, const wxString &aDestinationURL) override
Create a clickable hyperlink with a rectangular click area.
virtual void PenTo(const VECTOR2I &pos, char plume) override
Moveto/lineto primitive, moves the 'pen' to the specified direction.
std::map< wxString, std::vector< std::pair< BOX2I, wxString > > > m_bookmarksInPage
virtual bool StartPlot(const wxString &aPageNumber) override
The PDF engine supports multiple pages; the first one is opened 'for free' the following are to be cl...
virtual void StartPage(const wxString &aPageNumber, const wxString &aPageName=wxEmptyString, const wxString &aParentPageNumber=wxEmptyString, const wxString &aParentPageName=wxEmptyString)
Start a new page in the PDF document.
std::unique_ptr< PDF_OUTLINE_FONT_MANAGER > m_outlineFontManager
OUTLINE_NODE * addOutlineNode(OUTLINE_NODE *aParent, int aActionHandle, const wxString &aTitle)
Add a new outline node entry.
int m_imgResDictHandle
Image resource dictionary.
std::string encodeStringForPlotter(const wxString &aUnicode) override
convert a wxString unicode string to a char string compatible with the accepted string PDF format (co...
std::unique_ptr< PDF_STROKE_FONT_MANAGER > m_strokeFontManager
void Plot3DModel(const wxString &aSourcePath, const std::vector< PDF_3D_VIEW > &a3DViews)
std::vector< wxString > m_pageNumbers
List of user-space page numbers for resolving internal hyperlinks.
int allocPdfObject()
Allocate a new handle in the table of the PDF object.
VECTOR2I renderWord(const wxString &aWord, const VECTOR2I &aPosition, const VECTOR2I &aSize, const EDA_ANGLE &aOrient, bool aTextMirrored, int aWidth, bool aBold, bool aItalic, KIFONT::FONT *aFont, const KIFONT::METRICS &aFontMetrics, enum GR_TEXT_V_ALIGN_T aV_justify, TEXT_STYLE_FLAGS aTextStyle)
Render a single word with the given style parameters.
wxString m_workFilename
std::string encodeByteString(const std::string &aBytes)
void drawOverbars(const std::vector< OVERBAR_INFO > &aOverbars, const EDA_ANGLE &aOrient, const KIFONT::METRICS &aFontMetrics)
Draw overbar lines above text.
virtual void SetViewport(const VECTOR2I &aOffset, double aIusPerDecimil, double aScale, bool aMirror) override
PDF can have multiple pages, so SetPageSettings can be called with the outputFile open (but not insid...
std::map< int, std::pair< BOX2D, std::vector< wxString > > > m_hyperlinkMenuHandles
int m_pageStreamHandle
Handle of the page content object.
virtual void PlotText(const VECTOR2I &aPos, const COLOR4D &aColor, const wxString &aText, const TEXT_ATTRIBUTES &aAttributes, KIFONT::FONT *aFont, const KIFONT::METRICS &aFontMetrics, void *aData=nullptr) override
static std::vector< float > CreateC2WMatrixFromAngles(const VECTOR3D &aTargetPosition, float aCameraDistance, float aYawDegrees, float aPitchDegrees, float aRollDegrees)
Generates the camera to world matrix for use with a 3D View.
std::vector< std::pair< BOX2I, wxString > > m_hyperlinksInPage
List of loaded hyperlinks in current page.
std::unique_ptr< OUTLINE_NODE > m_outlineRoot
Root outline node.
std::vector< std::pair< BOX2I, std::vector< wxString > > > m_hyperlinkMenusInPage
void closePdfObject()
Close the current PDF object.
int m_totalOutlineNodes
Total number of outline nodes.
std::vector< VECTOR2D > arcPath(const VECTOR2D &aCenter, const EDA_ANGLE &aStartAngle, const EDA_ANGLE &aAngle, double aRadius)
const std::string & ResourceName() const
std::string BuildDifferencesArray() const
double FontBBoxMaxY() const
double FontBBoxMinY() const
double UnitsPerEm() const
double FontBBoxMinX() const
void SetToUnicodeHandle(int aHandle)
std::string BuildWidthsArray() const
double FontBBoxMaxX() const
void SetFontHandle(int aHandle)
void SetCharProcsHandle(int aHandle)
std::string BuildToUnicodeCMap() const
std::vector< GLYPH > & Glyphs()
double GetDotMarkLenIU(int aLineWidth) const
Definition plotter.cpp:134
double GetDashGapLenIU(int aLineWidth) const
Definition plotter.cpp:146
const PROJECT * m_project
Definition plotter.h:727
wxString m_subject
Definition plotter.h:719
bool m_mirrorIsHorizontal
Definition plotter.h:702
PAGE_INFO m_pageInfo
Definition plotter.h:720
bool m_plotMirror
Definition plotter.h:700
static const int USE_DEFAULT_LINE_WIDTH
Definition plotter.h:140
void MoveTo(const VECTOR2I &pos)
Definition plotter.h:308
void FinishTo(const VECTOR2I &pos)
Definition plotter.h:318
wxString m_author
Definition plotter.h:718
double m_iuPerDeviceUnit
Definition plotter.h:697
VECTOR2I m_plotOffset
Definition plotter.h:699
VECTOR2I m_penLastpos
Definition plotter.h:713
virtual VECTOR2D userToDeviceCoordinates(const VECTOR2I &aCoordinate)
Modify coordinates according to the orientation, scale factor, and offsets trace.
Definition plotter.cpp:93
VECTOR2I m_paperSize
Definition plotter.h:721
virtual VECTOR2D userToDeviceSize(const VECTOR2I &size)
Modify size according to the plotter scale factors (VECTOR2I version, returns a VECTOR2D).
Definition plotter.cpp:118
char m_penState
Definition plotter.h:712
wxString m_creator
Definition plotter.h:715
int m_currentPenWidth
Definition plotter.h:711
double m_plotScale
Plot scale - chosen by the user (even implicitly with 'fit in a4')
Definition plotter.h:689
FILE * m_outputFile
Output file.
Definition plotter.h:706
void LineTo(const VECTOR2I &pos)
Definition plotter.h:313
void PenFinish()
Definition plotter.h:324
static const int DO_NOT_SET_LINE_WIDTH
Definition plotter.h:139
RENDER_SETTINGS * m_renderSettings
Definition plotter.h:725
double m_IUsPerDecimil
Definition plotter.h:695
wxString m_title
Definition plotter.h:717
virtual int GetCurrentLineWidth() const
Definition plotter.h:182
bool m_colorMode
Definition plotter.h:709
double GetDashMarkLenIU(int aLineWidth) const
Definition plotter.cpp:140
wxString m_filename
Definition plotter.h:716
virtual void SetColor(const COLOR4D &color) override
The SetColor implementation is split with the subclasses: the PSLIKE computes the rgb values,...
double plotScaleAdjX
Fine user scale adjust ( = 1.0 if no correction)
void computeTextParameters(const VECTOR2I &aPos, const wxString &aText, const EDA_ANGLE &aOrient, const VECTOR2I &aSize, bool aMirror, enum GR_TEXT_H_ALIGN_T aH_justify, enum GR_TEXT_V_ALIGN_T aV_justify, int aWidth, bool aItalic, bool aBold, double *wideningFactor, double *ctm_a, double *ctm_b, double *ctm_c, double *ctm_d, double *ctm_e, double *ctm_f, double *heightFactor)
This is the core for postscript/PDF text alignment.
Definition seg.h:42
VECTOR2I A
Definition seg.h:49
VECTOR2I B
Definition seg.h:50
EDA_ANGLE GetCentralAngle() const
Get the "central angle" of the arc - this is the angle at the point of the "pie slice".
double GetRadius() const
EDA_ANGLE GetStartAngle() const
const VECTOR2I & GetCenter() const
Represent a polyline containing arcs as well as line segments: A chain of connected line and/or arc s...
const SHAPE_ARC & Arc(size_t aArc) const
ssize_t ArcIndex(size_t aSegment) const
Return the arc index for the given segment index.
SEG Segment(int aIndex) const
Return a copy of the aIndex-th segment in the line chain.
int SegmentCount() const
Return the number of segments in this line chain.
bool IsArcSegment(size_t aSegment) const
const SHAPE_LINE_CHAIN Outline() const
void SetRadius(int aRadius)
Definition shape_rect.h:206
GR_TEXT_H_ALIGN_T m_Halign
GR_TEXT_V_ALIGN_T m_Valign
An 8 bit string that is assuredly encoded in UTF8, and supplies special conversion support to and fro...
Definition utf8.h:71
std::string substr(size_t pos=0, size_t len=npos) const
Definition utf8.h:205
const wxString ResolveUriByEnvVars(const wxString &aUri, const PROJECT *aProject)
Replace any environment and/or text variables in URIs.
Definition common.cpp:571
The common library.
#define _(s)
@ DEGREES_T
Definition eda_angle.h:31
FILL_T
Definition eda_shape.h:56
@ NO_FILL
Definition eda_shape.h:57
@ FILLED_SHAPE
Fill with object color.
Definition eda_shape.h:58
@ BOLD
Definition font.h:46
@ SUBSCRIPT
Definition font.h:48
@ ITALIC
Definition font.h:47
@ SUPERSCRIPT
Definition font.h:49
unsigned int TEXT_STYLE_FLAGS
Definition font.h:64
static constexpr double ITALIC_TILT
Tilt factor for italic style (this is the scaling factor on dY relative coordinates to give a tilted ...
Definition font.h:61
const wxChar *const tracePdfPlotter
Flag to enable PDF plotter debug tracing.
void ignore_unused(const T &)
Definition ignore.h:24
This file contains miscellaneous commonly used macros and functions.
#define KI_FALLTHROUGH
The KI_FALLTHROUGH macro is to be used when switch statement cases should purposely fallthrough from ...
Definition macros.h:83
EDA_ANGLE abs(const EDA_ANGLE &aAngle)
Definition eda_angle.h:400
Plotting engines similar to ps (PostScript, Gerber, svg)
wxString NormalizeFileUri(const wxString &aFileUri)
Normalize file path aFileUri to URI convention.
wxString EscapeString(const wxString &aSource, ESCAPE_CONTEXT aContext)
The Escape/Unescape routines use HTML-entity-reference-style encoding to handle characters which are:...
#define TO_UTF8(wxstring)
Convert a wxString to a UTF8 encoded C string for all wxWidgets build modes.
@ CTX_JS_STR
LINE_STYLE
Dashed line types.
bool isOverbar() const
bool isSuperscript() const
wxString asWxString() const
bool isSubscript() const
wxString title
Title of outline node.
std::vector< OUTLINE_NODE * > children
Ordered list of children.
int entryHandle
Allocated handle for this outline entry.
OUTLINE_NODE * AddChild(int aActionHandle, const wxString &aTitle, int aEntryHandle)
int actionHandle
Handle to action.
std::string path
int radius
VECTOR2I end
wxString result
Test unit parsing edge cases and error handling.
int delta
GR_TEXT_H_ALIGN_T
This is API surface mapped to common.types.HorizontalAlignment.
@ GR_TEXT_H_ALIGN_CENTER
@ GR_TEXT_H_ALIGN_RIGHT
@ GR_TEXT_H_ALIGN_LEFT
GR_TEXT_V_ALIGN_T
This is API surface mapped to common.types.VertialAlignment.
@ GR_TEXT_V_ALIGN_BOTTOM
@ GR_TEXT_V_ALIGN_CENTER
@ GR_TEXT_V_ALIGN_TOP
#define M_PI
wxLogTrace helper definitions.
void RotatePoint(int *pX, int *pY, const EDA_ANGLE &aAngle)
Calculate the new point of coord coord pX, pY, for a rotation center 0, 0.
Definition trigo.cpp:229
VECTOR2< int32_t > VECTOR2I
Definition vector2d.h:695
VECTOR2< double > VECTOR2D
Definition vector2d.h:694
VECTOR3< double > VECTOR3D
Definition vector3.h:230