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