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