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