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