You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2632 lines
86 KiB
2632 lines
86 KiB
/*
|
|
* This program source code file is part of KiCad, a free EDA CAD application.
|
|
*
|
|
* Copyright (C) 1992-2012 Lorenzo Marcantonio, l.marcantonio@logossrl.com
|
|
* Copyright The KiCad Developers, see AUTHORS.txt for contributors.
|
|
*
|
|
* This program is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU General Public License
|
|
* as published by the Free Software Foundation; either version 2
|
|
* of the License, or (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; if not, you may find one here:
|
|
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
|
* or you may search the http://www.gnu.org website for the version 2 license,
|
|
* or you may write to the Free Software Foundation, Inc.,
|
|
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
|
|
*/
|
|
|
|
#include <algorithm>
|
|
#include <iterator>
|
|
#include <cstdio> // snprintf
|
|
#include <stack>
|
|
|
|
#include <wx/filename.h>
|
|
#include <wx/mstream.h>
|
|
#include <wx/zstream.h>
|
|
#include <wx/wfstream.h>
|
|
#include <wx/datstrm.h>
|
|
#include <wx/tokenzr.h>
|
|
|
|
#include <advanced_config.h>
|
|
#include <common.h> // ResolveUriByEnvVars
|
|
#include <eda_text.h> // for IsGotoPageHref
|
|
#include <font/font.h>
|
|
#include <core/ignore.h>
|
|
#include <macros.h>
|
|
#include <trace_helpers.h>
|
|
#include <trigo.h>
|
|
#include <string_utils.h>
|
|
#include <markup_parser.h>
|
|
#include <fmt/format.h>
|
|
#include <fmt/chrono.h>
|
|
#include <fmt/ranges.h>
|
|
|
|
#include <plotters/pdf_stroke_font.h>
|
|
#include <plotters/pdf_outline_font.h>
|
|
#include <plotters/plotters_pslike.h>
|
|
#include <geometry/shape_rect.h>
|
|
#include <text_eval/text_eval_wrapper.h>
|
|
|
|
|
|
PDF_PLOTTER::~PDF_PLOTTER() = default;
|
|
|
|
|
|
std::string PDF_PLOTTER::encodeStringForPlotter( const wxString& aText )
|
|
{
|
|
// returns a string compatible with PDF string convention from a unicode string.
|
|
// if the initial text is only ASCII7, return the text between ( and ) for a good readability
|
|
// if the initial text is no ASCII7, return the text between < and >
|
|
// and encoded using 16 bits hexa (4 digits) by wide char (unicode 16)
|
|
std::string result;
|
|
|
|
// Is aText only ASCII7 ?
|
|
bool is_ascii7 = true;
|
|
|
|
for( size_t ii = 0; ii < aText.Len(); ii++ )
|
|
{
|
|
if( aText[ii] >= 0x7F )
|
|
{
|
|
is_ascii7 = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if( is_ascii7 )
|
|
{
|
|
result = '(';
|
|
|
|
for( unsigned ii = 0; ii < aText.Len(); ii++ )
|
|
{
|
|
unsigned int code = aText[ii];
|
|
|
|
// These characters must be escaped
|
|
switch( code )
|
|
{
|
|
case '(':
|
|
case ')':
|
|
case '\\':
|
|
result += '\\';
|
|
KI_FALLTHROUGH;
|
|
|
|
default:
|
|
result += code;
|
|
break;
|
|
}
|
|
}
|
|
|
|
result += ')';
|
|
}
|
|
else
|
|
{
|
|
result = "<FEFF";
|
|
|
|
for( size_t ii = 0; ii < aText.Len(); ii++ )
|
|
{
|
|
unsigned int code = aText[ii];
|
|
result += fmt::format("{:04X}", code);
|
|
}
|
|
|
|
result += '>';
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
|
|
std::string PDF_PLOTTER::encodeByteString( const std::string& aBytes )
|
|
{
|
|
std::string result;
|
|
result.reserve( aBytes.size() * 4 + 2 );
|
|
result.push_back( '(' );
|
|
|
|
for( unsigned char byte : aBytes )
|
|
{
|
|
if( byte == '(' || byte == ')' || byte == '\\' )
|
|
{
|
|
result.push_back( '\\' );
|
|
result.push_back( static_cast<char>( byte ) );
|
|
}
|
|
else if( byte < 32 || byte > 126 )
|
|
{
|
|
fmt::format_to( std::back_inserter( result ), "\\{:03o}", byte );
|
|
}
|
|
else
|
|
{
|
|
result.push_back( static_cast<char>( byte ) );
|
|
}
|
|
}
|
|
|
|
result.push_back( ')' );
|
|
return result;
|
|
}
|
|
|
|
|
|
bool PDF_PLOTTER::OpenFile( const wxString& aFullFilename )
|
|
{
|
|
m_filename = aFullFilename;
|
|
|
|
wxASSERT( !m_outputFile );
|
|
|
|
// Open the PDF file in binary mode
|
|
m_outputFile = wxFopen( m_filename, wxT( "wb" ) );
|
|
|
|
if( m_outputFile == nullptr )
|
|
return false ;
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
void PDF_PLOTTER::SetViewport( const VECTOR2I& aOffset, double aIusPerDecimil,
|
|
double aScale, bool aMirror )
|
|
{
|
|
m_plotMirror = aMirror;
|
|
m_plotOffset = aOffset;
|
|
m_plotScale = aScale;
|
|
m_IUsPerDecimil = aIusPerDecimil;
|
|
|
|
// The CTM is set to 1 user unit per decimal
|
|
m_iuPerDeviceUnit = 1.0 / aIusPerDecimil;
|
|
|
|
/* The paper size in this engine is handled page by page
|
|
Look in the StartPage function */
|
|
}
|
|
|
|
|
|
void PDF_PLOTTER::SetCurrentLineWidth( int aWidth, void* aData )
|
|
{
|
|
wxASSERT( m_workFile );
|
|
|
|
if( aWidth == DO_NOT_SET_LINE_WIDTH )
|
|
return;
|
|
else if( aWidth == USE_DEFAULT_LINE_WIDTH )
|
|
aWidth = m_renderSettings->GetDefaultPenWidth();
|
|
|
|
if( aWidth == 0 )
|
|
aWidth = 1;
|
|
|
|
wxASSERT_MSG( aWidth > 0, "Plotter called to set negative pen width" );
|
|
|
|
if( aWidth != m_currentPenWidth )
|
|
fmt::println( m_workFile, "{:g} w", userToDeviceSize( aWidth ) );
|
|
|
|
m_currentPenWidth = aWidth;
|
|
}
|
|
|
|
|
|
void PDF_PLOTTER::emitSetRGBColor( double r, double g, double b, double a )
|
|
{
|
|
wxASSERT( m_workFile );
|
|
|
|
// PDF treats all colors as opaque, so the best we can do with alpha is generate an
|
|
// appropriate blended color assuming white paper.
|
|
if( a < 1.0 )
|
|
{
|
|
r = ( r * a ) + ( 1 - a );
|
|
g = ( g * a ) + ( 1 - a );
|
|
b = ( b * a ) + ( 1 - a );
|
|
}
|
|
|
|
fmt::println( m_workFile, "{:g} {:g} {:g} rg {:g} {:g} {:g} RG", r, g, b, r, g, b );
|
|
}
|
|
|
|
|
|
void PDF_PLOTTER::SetDash( int aLineWidth, LINE_STYLE aLineStyle )
|
|
{
|
|
wxASSERT( m_workFile );
|
|
|
|
switch( aLineStyle )
|
|
{
|
|
case LINE_STYLE::DASH:
|
|
fmt::println( m_workFile, "[{} {}] 0 d",
|
|
(int) GetDashMarkLenIU( aLineWidth ),
|
|
(int) GetDashGapLenIU( aLineWidth ) );
|
|
break;
|
|
|
|
case LINE_STYLE::DOT:
|
|
fmt::println( m_workFile, "[{} {}] 0 d",
|
|
(int) GetDotMarkLenIU( aLineWidth ),
|
|
(int) GetDashGapLenIU( aLineWidth ) );
|
|
break;
|
|
|
|
case LINE_STYLE::DASHDOT:
|
|
fmt::println( m_workFile, "[{} {} {} {}] 0 d",
|
|
(int) GetDashMarkLenIU( aLineWidth ),
|
|
(int) GetDashGapLenIU( aLineWidth ),
|
|
(int) GetDotMarkLenIU( aLineWidth ),
|
|
(int) GetDashGapLenIU( aLineWidth ) );
|
|
break;
|
|
|
|
case LINE_STYLE::DASHDOTDOT:
|
|
fmt::println( m_workFile, "[{} {} {} {} {} {}] 0 d",
|
|
(int) GetDashMarkLenIU( aLineWidth ),
|
|
(int) GetDashGapLenIU( aLineWidth ),
|
|
(int) GetDotMarkLenIU( aLineWidth ),
|
|
(int) GetDashGapLenIU( aLineWidth ),
|
|
(int) GetDotMarkLenIU( aLineWidth ),
|
|
(int) GetDashGapLenIU( aLineWidth ) );
|
|
break;
|
|
|
|
default:
|
|
fmt::println( m_workFile, "[] 0 d\n" );
|
|
}
|
|
}
|
|
|
|
|
|
void PDF_PLOTTER::Rect( const VECTOR2I& p1, const VECTOR2I& p2, FILL_T fill, int width,
|
|
int aCornerRadius )
|
|
{
|
|
wxASSERT( m_workFile );
|
|
|
|
if( fill == FILL_T::NO_FILL && width == 0 )
|
|
return;
|
|
|
|
SetCurrentLineWidth( width );
|
|
|
|
if( aCornerRadius > 0 )
|
|
{
|
|
BOX2I box( p1, VECTOR2I( p2.x - p1.x, p2.y - p1.y ) );
|
|
box.Normalize();
|
|
SHAPE_RECT rect( box );
|
|
rect.SetRadius( aCornerRadius );
|
|
PlotPoly( rect.Outline(), fill, width, nullptr );
|
|
return;
|
|
}
|
|
|
|
VECTOR2I size = p2 - p1;
|
|
|
|
if( size.x == 0 && size.y == 0 )
|
|
{
|
|
// Can't draw zero-sized rectangles
|
|
MoveTo( VECTOR2I( p1.x, p1.y ) );
|
|
FinishTo( VECTOR2I( p1.x, p1.y ) );
|
|
|
|
return;
|
|
}
|
|
|
|
if( std::min( std::abs( size.x ), std::abs( size.y ) ) < width )
|
|
{
|
|
// Too thick stroked rectangles are buggy, draw as polygon
|
|
std::vector<VECTOR2I> cornerList;
|
|
|
|
cornerList.emplace_back( p1.x, p1.y );
|
|
cornerList.emplace_back( p2.x, p1.y );
|
|
cornerList.emplace_back( p2.x, p2.y );
|
|
cornerList.emplace_back( p1.x, p2.y );
|
|
cornerList.emplace_back( p1.x, p1.y );
|
|
|
|
PlotPoly( cornerList, fill, width, nullptr );
|
|
|
|
return;
|
|
}
|
|
|
|
VECTOR2D p1_dev = userToDeviceCoordinates( p1 );
|
|
VECTOR2D p2_dev = userToDeviceCoordinates( p2 );
|
|
|
|
char paintOp;
|
|
|
|
if( fill == FILL_T::NO_FILL )
|
|
paintOp = 'S';
|
|
else
|
|
paintOp = width > 0 ? 'B' : 'f';
|
|
|
|
fmt::println( m_workFile, "{:g} {:g} {:g} {:g} re {}",
|
|
p1_dev.x,
|
|
p1_dev.y,
|
|
p2_dev.x - p1_dev.x,
|
|
p2_dev.y - p1_dev.y,
|
|
paintOp );
|
|
}
|
|
|
|
|
|
void PDF_PLOTTER::Circle( const VECTOR2I& pos, int diametre, FILL_T aFill, int width )
|
|
{
|
|
wxASSERT( m_workFile );
|
|
|
|
if( aFill == FILL_T::NO_FILL && width == 0 )
|
|
return;
|
|
|
|
SetCurrentLineWidth( width );
|
|
|
|
VECTOR2D pos_dev = userToDeviceCoordinates( pos );
|
|
double radius = userToDeviceSize( diametre / 2.0 );
|
|
|
|
// If diameter is less than width, switch to filled mode
|
|
if( aFill == FILL_T::NO_FILL && diametre < GetCurrentLineWidth() )
|
|
{
|
|
aFill = FILL_T::FILLED_SHAPE;
|
|
radius = userToDeviceSize( ( diametre / 2.0 ) + ( width / 2.0 ) );
|
|
}
|
|
|
|
/* OK. Here's a trick. PDF doesn't support circles or circular angles, that's
|
|
a fact. You'll have to do with cubic beziers. These *can't* represent
|
|
circular arcs (NURBS can, beziers don't). But there is a widely known
|
|
approximation which is really good
|
|
*/
|
|
|
|
double magic = radius * 0.551784; // You don't want to know where this come from
|
|
|
|
// This is the convex hull for the bezier approximated circle
|
|
fmt::println( m_workFile,
|
|
"{:g} {:g} m "
|
|
"{:g} {:g} {:g} {:g} {:g} {:g} c "
|
|
"{:g} {:g} {:g} {:g} {:g} {:g} c "
|
|
"{:g} {:g} {:g} {:g} {:g} {:g} c "
|
|
"{:g} {:g} {:g} {:g} {:g} {:g} c {}",
|
|
pos_dev.x - radius, pos_dev.y,
|
|
|
|
pos_dev.x - radius, pos_dev.y + magic,
|
|
pos_dev.x - magic, pos_dev.y + radius,
|
|
pos_dev.x, pos_dev.y + radius,
|
|
|
|
pos_dev.x + magic, pos_dev.y + radius,
|
|
pos_dev.x + radius, pos_dev.y + magic,
|
|
pos_dev.x + radius, pos_dev.y,
|
|
|
|
pos_dev.x + radius, pos_dev.y - magic,
|
|
pos_dev.x + magic, pos_dev.y - radius,
|
|
pos_dev.x, pos_dev.y - radius,
|
|
|
|
pos_dev.x - magic, pos_dev.y - radius,
|
|
pos_dev.x - radius, pos_dev.y - magic,
|
|
pos_dev.x - radius, pos_dev.y,
|
|
|
|
aFill == FILL_T::NO_FILL ? 's' : 'b' );
|
|
}
|
|
|
|
|
|
void PDF_PLOTTER::Arc( const VECTOR2D& aCenter, const EDA_ANGLE& aStartAngle,
|
|
const EDA_ANGLE& aAngle, double aRadius, FILL_T aFill, int aWidth )
|
|
{
|
|
wxASSERT( m_workFile );
|
|
|
|
SetCurrentLineWidth( aWidth );
|
|
|
|
if( aRadius <= 0 )
|
|
{
|
|
Circle( aCenter, GetCurrentLineWidth(), FILL_T::FILLED_SHAPE, 0 );
|
|
return;
|
|
}
|
|
|
|
/*
|
|
* Arcs are not so easily approximated by beziers (in the general case), so we approximate
|
|
* them in the old way
|
|
*/
|
|
EDA_ANGLE startAngle = -aStartAngle;
|
|
EDA_ANGLE endAngle = startAngle - aAngle;
|
|
VECTOR2I start;
|
|
VECTOR2I end;
|
|
const EDA_ANGLE delta( 5, DEGREES_T ); // increment to draw circles
|
|
|
|
if( startAngle > endAngle )
|
|
std::swap( startAngle, endAngle );
|
|
|
|
// Usual trig arc plotting routine...
|
|
start.x = KiROUND( aCenter.x + aRadius * ( -startAngle ).Cos() );
|
|
start.y = KiROUND( aCenter.y + aRadius * ( -startAngle ).Sin() );
|
|
VECTOR2D pos_dev = userToDeviceCoordinates( start );
|
|
fmt::print( m_workFile, "{:g} {:g} m ", pos_dev.x, pos_dev.y );
|
|
|
|
for( EDA_ANGLE ii = startAngle + delta; ii < endAngle; ii += delta )
|
|
{
|
|
end.x = KiROUND( aCenter.x + aRadius * ( -ii ).Cos() );
|
|
end.y = KiROUND( aCenter.y + aRadius * ( -ii ).Sin() );
|
|
pos_dev = userToDeviceCoordinates( end );
|
|
fmt::print( m_workFile, "{:g} {:g} l ", pos_dev.x, pos_dev.y );
|
|
}
|
|
|
|
end.x = KiROUND( aCenter.x + aRadius * ( -endAngle ).Cos() );
|
|
end.y = KiROUND( aCenter.y + aRadius * ( -endAngle ).Sin() );
|
|
pos_dev = userToDeviceCoordinates( end );
|
|
fmt::print( m_workFile, "{:g} {:g} l ", pos_dev.x, pos_dev.y );
|
|
|
|
// The arc is drawn... if not filled we stroke it, otherwise we finish
|
|
// closing the pie at the center
|
|
if( aFill == FILL_T::NO_FILL )
|
|
{
|
|
fmt::println( m_workFile, "S" );
|
|
}
|
|
else
|
|
{
|
|
pos_dev = userToDeviceCoordinates( aCenter );
|
|
fmt::println( m_workFile, "{:g} {:g} l b", pos_dev.x, pos_dev.y );
|
|
}
|
|
}
|
|
|
|
|
|
void PDF_PLOTTER::PlotPoly( const std::vector<VECTOR2I>& aCornerList, FILL_T aFill, int aWidth,
|
|
void* aData )
|
|
{
|
|
wxASSERT( m_workFile );
|
|
|
|
if( aCornerList.size() <= 1 )
|
|
return;
|
|
|
|
if( aFill == FILL_T::NO_FILL && aWidth == 0 )
|
|
return;
|
|
|
|
SetCurrentLineWidth( aWidth );
|
|
|
|
VECTOR2D pos = userToDeviceCoordinates( aCornerList[0] );
|
|
fmt::println( m_workFile, "{:f} {:f} m", pos.x, pos.y );
|
|
|
|
for( unsigned ii = 1; ii < aCornerList.size(); ii++ )
|
|
{
|
|
pos = userToDeviceCoordinates( aCornerList[ii] );
|
|
fmt::println( m_workFile, "{:f} {:f} l", pos.x, pos.y );
|
|
}
|
|
|
|
// Close path and stroke and/or fill
|
|
if( aFill == FILL_T::NO_FILL )
|
|
fmt::println( m_workFile, "S" );
|
|
else if( aWidth == 0 )
|
|
fmt::println( m_workFile, "f" );
|
|
else
|
|
fmt::println( m_workFile, "b" );
|
|
}
|
|
|
|
|
|
void PDF_PLOTTER::PlotPoly( const SHAPE_LINE_CHAIN& aCornerList, FILL_T aFill, int aWidth,
|
|
void* aData )
|
|
{
|
|
std::vector<VECTOR2I> cornerList;
|
|
cornerList.reserve( aCornerList.PointCount() );
|
|
|
|
for( int ii = 0; ii < aCornerList.PointCount(); ii++ )
|
|
cornerList.emplace_back( aCornerList.CPoint( ii ) );
|
|
|
|
if( aCornerList.IsClosed() && cornerList.front() != cornerList.back() )
|
|
cornerList.emplace_back( aCornerList.CPoint( 0 ) );
|
|
|
|
PlotPoly( cornerList, aFill, aWidth, aData );
|
|
}
|
|
|
|
|
|
void PDF_PLOTTER::PenTo( const VECTOR2I& pos, char plume )
|
|
{
|
|
wxASSERT( m_workFile );
|
|
|
|
if( plume == 'Z' )
|
|
{
|
|
if( m_penState != 'Z' )
|
|
{
|
|
fmt::println( m_workFile, "S" );
|
|
m_penState = 'Z';
|
|
m_penLastpos.x = -1;
|
|
m_penLastpos.y = -1;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if( m_penState != plume || pos != m_penLastpos )
|
|
{
|
|
VECTOR2D pos_dev = userToDeviceCoordinates( pos );
|
|
fmt::println( m_workFile, "{:f} {:f} {}",
|
|
pos_dev.x,
|
|
pos_dev.y,
|
|
plume == 'D' ? 'l' : 'm' );
|
|
}
|
|
|
|
m_penState = plume;
|
|
m_penLastpos = pos;
|
|
}
|
|
|
|
|
|
void PDF_PLOTTER::PlotImage( const wxImage& aImage, const VECTOR2I& aPos, double aScaleFactor )
|
|
{
|
|
wxASSERT( m_workFile );
|
|
VECTOR2I pix_size( aImage.GetWidth(), aImage.GetHeight() );
|
|
|
|
// Requested size (in IUs)
|
|
VECTOR2D drawsize( aScaleFactor * pix_size.x, aScaleFactor * pix_size.y );
|
|
|
|
// calculate the bitmap start position
|
|
VECTOR2I start( aPos.x - drawsize.x / 2, aPos.y + drawsize.y / 2 );
|
|
VECTOR2D dev_start = userToDeviceCoordinates( start );
|
|
|
|
// Deduplicate images
|
|
auto findHandleForImage = [&]( const wxImage& aCurrImage ) -> int
|
|
{
|
|
for( const auto& [imgHandle, image] : m_imageHandles )
|
|
{
|
|
if( image.IsSameAs( aCurrImage ) )
|
|
return imgHandle;
|
|
|
|
if( image.GetWidth() != aCurrImage.GetWidth() )
|
|
continue;
|
|
|
|
if( image.GetHeight() != aCurrImage.GetHeight() )
|
|
continue;
|
|
|
|
if( image.GetType() != aCurrImage.GetType() )
|
|
continue;
|
|
|
|
if( image.HasAlpha() != aCurrImage.HasAlpha() )
|
|
continue;
|
|
|
|
if( image.HasMask() != aCurrImage.HasMask()
|
|
|| image.GetMaskRed() != aCurrImage.GetMaskRed()
|
|
|| image.GetMaskGreen() != aCurrImage.GetMaskGreen()
|
|
|| image.GetMaskBlue() != aCurrImage.GetMaskBlue() )
|
|
continue;
|
|
|
|
int pixCount = image.GetWidth() * image.GetHeight();
|
|
|
|
if( memcmp( image.GetData(), aCurrImage.GetData(), pixCount * 3 ) != 0 )
|
|
continue;
|
|
|
|
if( image.HasAlpha() && memcmp( image.GetAlpha(), aCurrImage.GetAlpha(), pixCount ) != 0 )
|
|
continue;
|
|
|
|
return imgHandle;
|
|
}
|
|
|
|
return -1;
|
|
};
|
|
|
|
int imgHandle = findHandleForImage( aImage );
|
|
|
|
if( imgHandle == -1 )
|
|
{
|
|
imgHandle = allocPdfObject();
|
|
m_imageHandles.emplace( imgHandle, aImage );
|
|
}
|
|
|
|
/* PDF has an uhm... simplified coordinate system handling. There is
|
|
*one* operator to do everything (the PS concat equivalent). At least
|
|
they kept the matrix stack to save restore environments. Also images
|
|
are always emitted at the origin with a size of 1x1 user units.
|
|
What we need to do is:
|
|
1) save the CTM end establish the new one
|
|
2) plot the image
|
|
3) restore the CTM
|
|
4) profit
|
|
*/
|
|
fmt::println( m_workFile, "q {:g} 0 0 {:g} {:g} {:g} cm", // Step 1
|
|
userToDeviceSize( drawsize.x ),
|
|
userToDeviceSize( drawsize.y ),
|
|
dev_start.x,
|
|
dev_start.y );
|
|
|
|
fmt::println( m_workFile, "/Im{} Do", imgHandle );
|
|
fmt::println( m_workFile, "Q" );
|
|
}
|
|
|
|
|
|
int PDF_PLOTTER::allocPdfObject()
|
|
{
|
|
m_xrefTable.push_back( 0 );
|
|
return m_xrefTable.size() - 1;
|
|
}
|
|
|
|
|
|
int PDF_PLOTTER::startPdfObject( int aHandle )
|
|
{
|
|
wxASSERT( m_outputFile );
|
|
wxASSERT( !m_workFile );
|
|
|
|
if( aHandle < 0 )
|
|
aHandle = allocPdfObject();
|
|
|
|
m_xrefTable[aHandle] = ftell( m_outputFile );
|
|
fmt::println( m_outputFile, "{} 0 obj", aHandle );
|
|
return aHandle;
|
|
}
|
|
|
|
|
|
void PDF_PLOTTER::closePdfObject()
|
|
{
|
|
wxASSERT( m_outputFile );
|
|
wxASSERT( !m_workFile );
|
|
fmt::println( m_outputFile, "endobj" );
|
|
}
|
|
|
|
|
|
int PDF_PLOTTER::startPdfStream( int aHandle )
|
|
{
|
|
wxASSERT( m_outputFile );
|
|
wxASSERT( !m_workFile );
|
|
int handle = startPdfObject( aHandle );
|
|
|
|
// This is guaranteed to be handle+1 but needs to be allocated since
|
|
// you could allocate more object during stream preparation
|
|
m_streamLengthHandle = allocPdfObject();
|
|
|
|
if( ADVANCED_CFG::GetCfg().m_DebugPDFWriter )
|
|
{
|
|
fmt::print( m_outputFile,
|
|
"<< /Length {} 0 R >>\nstream\n",
|
|
m_streamLengthHandle );
|
|
}
|
|
else
|
|
{
|
|
fmt::print( m_outputFile,
|
|
"<< /Length {} 0 R /Filter /FlateDecode >>\nstream\n",
|
|
m_streamLengthHandle );
|
|
}
|
|
|
|
// Open a temporary file to accumulate the stream
|
|
m_workFilename = wxFileName::CreateTempFileName( "" );
|
|
m_workFile = wxFopen( m_workFilename, wxT( "w+b" ) );
|
|
wxASSERT( m_workFile );
|
|
return handle;
|
|
}
|
|
|
|
|
|
void PDF_PLOTTER::closePdfStream()
|
|
{
|
|
wxASSERT( m_workFile );
|
|
|
|
long stream_len = ftell( m_workFile );
|
|
|
|
if( stream_len < 0 )
|
|
{
|
|
wxASSERT( false );
|
|
return;
|
|
}
|
|
|
|
// Rewind the file, read in the page stream and DEFLATE it
|
|
fseek( m_workFile, 0, SEEK_SET );
|
|
unsigned char *inbuf = new unsigned char[stream_len];
|
|
|
|
int rc = fread( inbuf, 1, stream_len, m_workFile );
|
|
wxASSERT( rc == stream_len );
|
|
ignore_unused( rc );
|
|
|
|
// We are done with the temporary file, junk it
|
|
fclose( m_workFile );
|
|
m_workFile = nullptr;
|
|
::wxRemoveFile( m_workFilename );
|
|
|
|
unsigned out_count;
|
|
|
|
if( ADVANCED_CFG::GetCfg().m_DebugPDFWriter )
|
|
{
|
|
out_count = stream_len;
|
|
fwrite( inbuf, out_count, 1, m_outputFile );
|
|
}
|
|
else
|
|
{
|
|
// NULL means memos owns the memory, but provide a hint on optimum size needed.
|
|
wxMemoryOutputStream memos( nullptr, std::max( 2000l, stream_len ) ) ;
|
|
|
|
{
|
|
/* Somewhat standard parameters to compress in DEFLATE. The PDF spec is
|
|
* misleading, it says it wants a DEFLATE stream but it really want a ZLIB
|
|
* stream! (a DEFLATE stream would be generated with -15 instead of 15)
|
|
* rc = deflateInit2( &zstrm, Z_BEST_COMPRESSION, Z_DEFLATED, 15,
|
|
* 8, Z_DEFAULT_STRATEGY );
|
|
*/
|
|
|
|
wxZlibOutputStream zos( memos, wxZ_BEST_COMPRESSION, wxZLIB_ZLIB );
|
|
|
|
zos.Write( inbuf, stream_len );
|
|
} // flush the zip stream using zos destructor
|
|
|
|
wxStreamBuffer* sb = memos.GetOutputStreamBuffer();
|
|
|
|
out_count = sb->Tell();
|
|
fwrite( sb->GetBufferStart(), 1, out_count, m_outputFile );
|
|
}
|
|
|
|
delete[] inbuf;
|
|
fmt::print( m_outputFile, "\nendstream\n" );
|
|
closePdfObject();
|
|
|
|
// Writing the deferred length as an indirect object
|
|
startPdfObject( m_streamLengthHandle );
|
|
fmt::println( m_outputFile, "{}", out_count );
|
|
closePdfObject();
|
|
}
|
|
|
|
|
|
void PDF_PLOTTER::StartPage( const wxString& aPageNumber, const wxString& aPageName,
|
|
const wxString& aParentPageNumber, const wxString& aParentPageName )
|
|
{
|
|
wxASSERT( m_outputFile );
|
|
wxASSERT( !m_workFile );
|
|
|
|
m_pageNumbers.push_back( aPageNumber );
|
|
m_pageName = aPageName.IsEmpty() ? wxString::Format( _( "Page %s" ),
|
|
aPageNumber )
|
|
: wxString::Format( _( "%s (Page %s)" ),
|
|
aPageName,
|
|
aPageNumber );
|
|
m_parentPageName = aParentPageName.IsEmpty() ? wxString::Format( _( "Page %s" ),
|
|
aParentPageNumber )
|
|
: wxString::Format( _( "%s (Page %s)" ),
|
|
aParentPageName,
|
|
aParentPageNumber );
|
|
|
|
// Compute the paper size in IUs
|
|
m_paperSize = m_pageInfo.GetSizeMils();
|
|
m_paperSize.x *= 10.0 / m_iuPerDeviceUnit;
|
|
m_paperSize.y *= 10.0 / m_iuPerDeviceUnit;
|
|
|
|
// Set m_currentPenWidth to a unused value to ensure the pen width
|
|
// will be initialized to a the right value in pdf file by the first item to plot
|
|
m_currentPenWidth = 0;
|
|
|
|
if( !m_3dExportMode )
|
|
{
|
|
// Open the content stream; the page object will go later
|
|
m_pageStreamHandle = startPdfStream();
|
|
|
|
/* Now, until ClosePage *everything* must be wrote in workFile, to be
|
|
compressed later in closePdfStream */
|
|
|
|
// Default graphic settings (coordinate system, default color and line style)
|
|
fmt::println( m_workFile,
|
|
"{:g} 0 0 {:g} 0 0 cm 1 J 1 j 0 0 0 rg 0 0 0 RG {:g} w",
|
|
0.0072 * plotScaleAdjX, 0.0072 * plotScaleAdjY,
|
|
userToDeviceSize( m_renderSettings->GetDefaultPenWidth() ) );
|
|
}
|
|
}
|
|
|
|
|
|
void WriteImageStream( const wxImage& aImage, wxDataOutputStream& aOut, wxColor bg, bool colorMode )
|
|
{
|
|
int w = aImage.GetWidth();
|
|
int h = aImage.GetHeight();
|
|
|
|
for( int y = 0; y < h; y++ )
|
|
{
|
|
for( int x = 0; x < w; x++ )
|
|
{
|
|
unsigned char r = aImage.GetRed( x, y ) & 0xFF;
|
|
unsigned char g = aImage.GetGreen( x, y ) & 0xFF;
|
|
unsigned char b = aImage.GetBlue( x, y ) & 0xFF;
|
|
|
|
if( aImage.HasMask() )
|
|
{
|
|
if( r == aImage.GetMaskRed() && g == aImage.GetMaskGreen()
|
|
&& b == aImage.GetMaskBlue() )
|
|
{
|
|
r = bg.Red();
|
|
g = bg.Green();
|
|
b = bg.Blue();
|
|
}
|
|
}
|
|
|
|
if( colorMode )
|
|
{
|
|
aOut.Write8( r );
|
|
aOut.Write8( g );
|
|
aOut.Write8( b );
|
|
}
|
|
else
|
|
{
|
|
// Greyscale conversion (CIE 1931)
|
|
unsigned char grey = KiROUND( r * 0.2126 + g * 0.7152 + b * 0.0722 );
|
|
|
|
aOut.Write8( grey );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void WriteImageSMaskStream( const wxImage& aImage, wxDataOutputStream& aOut )
|
|
{
|
|
int w = aImage.GetWidth();
|
|
int h = aImage.GetHeight();
|
|
|
|
if( aImage.HasMask() )
|
|
{
|
|
for( int y = 0; y < h; y++ )
|
|
{
|
|
for( int x = 0; x < w; x++ )
|
|
{
|
|
unsigned char a = 255;
|
|
unsigned char r = aImage.GetRed( x, y );
|
|
unsigned char g = aImage.GetGreen( x, y );
|
|
unsigned char b = aImage.GetBlue( x, y );
|
|
|
|
if( r == aImage.GetMaskRed() && g == aImage.GetMaskGreen()
|
|
&& b == aImage.GetMaskBlue() )
|
|
{
|
|
a = 0;
|
|
}
|
|
|
|
aOut.Write8( a );
|
|
}
|
|
}
|
|
}
|
|
else if( aImage.HasAlpha() )
|
|
{
|
|
int size = w * h;
|
|
aOut.Write8( aImage.GetAlpha(), size );
|
|
}
|
|
}
|
|
|
|
|
|
void PDF_PLOTTER::ClosePage()
|
|
{
|
|
// non 3d exports need this
|
|
if( m_pageStreamHandle != -1 )
|
|
{
|
|
wxASSERT( m_workFile );
|
|
|
|
// Close the page stream (and compress it)
|
|
closePdfStream();
|
|
}
|
|
|
|
// Page size is in 1/72 of inch (default user space units). Works like the bbox in postscript
|
|
// but there is no need for swapping the sizes, since PDF doesn't require a portrait page.
|
|
// We use the MediaBox but PDF has lots of other less-used boxes that could be used.
|
|
const double PTsPERMIL = 0.072;
|
|
VECTOR2D psPaperSize = VECTOR2D( m_pageInfo.GetSizeMils() ) * PTsPERMIL;
|
|
|
|
auto iuToPdfUserSpace =
|
|
[&]( const VECTOR2I& aCoord ) -> VECTOR2D
|
|
{
|
|
VECTOR2D pos = VECTOR2D( aCoord ) * PTsPERMIL / ( m_IUsPerDecimil * 10 );
|
|
|
|
// PDF y=0 is at bottom of page, invert coordinate
|
|
VECTOR2D retval( pos.x, psPaperSize.y - pos.y );
|
|
|
|
// The pdf plot can be mirrored (from left to right). So mirror the
|
|
// x coordinate if m_plotMirror is set
|
|
if( m_plotMirror )
|
|
{
|
|
if( m_mirrorIsHorizontal )
|
|
retval.x = ( psPaperSize.x - pos.x );
|
|
else
|
|
retval.y = pos.y;
|
|
}
|
|
|
|
return retval;
|
|
};
|
|
|
|
// Handle annotations (at the moment only "link" type objects)
|
|
std::vector<int> annotHandles;
|
|
|
|
// Allocate all hyperlink objects for the page and calculate their position in user space
|
|
// coordinates
|
|
for( const std::pair<BOX2I, wxString>& linkPair : m_hyperlinksInPage )
|
|
{
|
|
const BOX2I& box = linkPair.first;
|
|
const wxString& url = linkPair.second;
|
|
|
|
VECTOR2D bottomLeft = iuToPdfUserSpace( box.GetPosition() );
|
|
VECTOR2D topRight = iuToPdfUserSpace( box.GetEnd() );
|
|
|
|
BOX2D userSpaceBox;
|
|
userSpaceBox.SetOrigin( bottomLeft );
|
|
userSpaceBox.SetEnd( topRight );
|
|
|
|
annotHandles.push_back( allocPdfObject() );
|
|
|
|
m_hyperlinkHandles.insert( { annotHandles.back(), { userSpaceBox, url } } );
|
|
}
|
|
|
|
for( const std::pair<BOX2I, std::vector<wxString>>& menuPair : m_hyperlinkMenusInPage )
|
|
{
|
|
const BOX2I& box = menuPair.first;
|
|
const std::vector<wxString>& urls = menuPair.second;
|
|
|
|
VECTOR2D bottomLeft = iuToPdfUserSpace( box.GetPosition() );
|
|
VECTOR2D topRight = iuToPdfUserSpace( box.GetEnd() );
|
|
|
|
BOX2D userSpaceBox;
|
|
userSpaceBox.SetOrigin( bottomLeft );
|
|
userSpaceBox.SetEnd( topRight );
|
|
|
|
annotHandles.push_back( allocPdfObject() );
|
|
|
|
m_hyperlinkMenuHandles.insert( { annotHandles.back(), { userSpaceBox, urls } } );
|
|
}
|
|
|
|
int annot3DHandle = -1;
|
|
|
|
if( m_3dExportMode )
|
|
{
|
|
annot3DHandle = allocPdfObject();
|
|
annotHandles.push_back( annot3DHandle );
|
|
}
|
|
|
|
|
|
int hyperLinkArrayHandle = -1;
|
|
|
|
// If we have added any annotation links, create an array containing all the objects
|
|
if( annotHandles.size() > 0 )
|
|
{
|
|
hyperLinkArrayHandle = startPdfObject();
|
|
bool isFirst = true;
|
|
|
|
fmt::print( m_outputFile, "[" );
|
|
|
|
for( int handle : annotHandles )
|
|
{
|
|
if( isFirst )
|
|
isFirst = false;
|
|
else
|
|
fmt::print( m_outputFile, " " );
|
|
|
|
fmt::print( m_outputFile, "{} 0 R", handle );
|
|
}
|
|
|
|
fmt::println( m_outputFile, "]" );
|
|
closePdfObject();
|
|
}
|
|
|
|
// Emit the page object and put it in the page list for later
|
|
int pageHandle = startPdfObject();
|
|
m_pageHandles.push_back( pageHandle );
|
|
|
|
fmt::print( m_outputFile,
|
|
"<<\n"
|
|
"/Type /Page\n"
|
|
"/Parent {} 0 R\n"
|
|
"/Resources <<\n"
|
|
" /ProcSet [/PDF /Text /ImageC /ImageB]\n"
|
|
" /Font {} 0 R\n"
|
|
" /XObject {} 0 R >>\n"
|
|
"/MediaBox [0 0 {:g} {:g}]\n"
|
|
"/Contents {} 0 R\n",
|
|
m_pageTreeHandle, m_fontResDictHandle, m_imgResDictHandle, psPaperSize.x, psPaperSize.y,
|
|
m_pageStreamHandle );
|
|
|
|
if( annotHandles.size() > 0 )
|
|
fmt::print( m_outputFile, "/Annots {} 0 R", hyperLinkArrayHandle );
|
|
|
|
fmt::print( m_outputFile, ">>\n" );
|
|
|
|
closePdfObject();
|
|
|
|
if( m_3dExportMode )
|
|
{
|
|
startPdfObject( annot3DHandle );
|
|
fmt::print( m_outputFile,
|
|
"<<\n"
|
|
"/Type /Annot\n"
|
|
"/Subtype /3D\n"
|
|
"/Rect [0 0 {:g} {:g}]\n"
|
|
"/NM (3D Annotation)\n"
|
|
"/3DD {} 0 R\n"
|
|
"/3DV 0\n"
|
|
"/3DA<</A/PO/D/PC/TB true/NP true>>\n"
|
|
"/3DI true\n"
|
|
"/P {} 0 R\n"
|
|
">>\n",
|
|
psPaperSize.x, psPaperSize.y, m_3dModelHandle, pageHandle );
|
|
|
|
closePdfObject();
|
|
}
|
|
|
|
// Mark the page stream as idle
|
|
m_pageStreamHandle = -1;
|
|
|
|
int actionHandle = emitGoToAction( pageHandle );
|
|
PDF_PLOTTER::OUTLINE_NODE* parent_node = m_outlineRoot.get();
|
|
|
|
if( !m_parentPageName.IsEmpty() )
|
|
{
|
|
// Search for the parent node iteratively through the entire tree
|
|
std::stack<OUTLINE_NODE*> nodes;
|
|
nodes.push( m_outlineRoot.get() );
|
|
|
|
while( !nodes.empty() )
|
|
{
|
|
OUTLINE_NODE* node = nodes.top();
|
|
nodes.pop();
|
|
|
|
// Check if this node matches
|
|
if( node->title == m_parentPageName )
|
|
{
|
|
parent_node = node;
|
|
break;
|
|
}
|
|
|
|
// Add all children to the stack
|
|
for( OUTLINE_NODE* child : node->children )
|
|
nodes.push( child );
|
|
}
|
|
}
|
|
|
|
OUTLINE_NODE* pageOutlineNode = addOutlineNode( parent_node, actionHandle, m_pageName );
|
|
|
|
// let's reorg the symbol bookmarks under a page handle
|
|
// let's reorg the symbol bookmarks under a page handle
|
|
for( const auto& [groupName, groupVector] : m_bookmarksInPage )
|
|
{
|
|
OUTLINE_NODE* groupOutlineNode = addOutlineNode( pageOutlineNode, actionHandle, groupName );
|
|
|
|
for( const std::pair<BOX2I, wxString>& bookmarkPair : groupVector )
|
|
{
|
|
const BOX2I& box = bookmarkPair.first;
|
|
const wxString& ref = bookmarkPair.second;
|
|
|
|
VECTOR2I bottomLeft = iuToPdfUserSpace( box.GetPosition() );
|
|
VECTOR2I topRight = iuToPdfUserSpace( box.GetEnd() );
|
|
|
|
actionHandle = emitGoToAction( pageHandle, bottomLeft, topRight );
|
|
|
|
addOutlineNode( groupOutlineNode, actionHandle, ref );
|
|
}
|
|
|
|
std::sort( groupOutlineNode->children.begin(), groupOutlineNode->children.end(),
|
|
[]( const OUTLINE_NODE* a, const OUTLINE_NODE* b ) -> bool
|
|
{
|
|
return a->title < b->title;
|
|
} );
|
|
}
|
|
|
|
// Clean up
|
|
m_hyperlinksInPage.clear();
|
|
m_hyperlinkMenusInPage.clear();
|
|
m_bookmarksInPage.clear();
|
|
}
|
|
|
|
|
|
bool PDF_PLOTTER::StartPlot( const wxString& aPageNumber )
|
|
{
|
|
return StartPlot( aPageNumber, wxEmptyString );
|
|
}
|
|
|
|
|
|
bool PDF_PLOTTER::StartPlot( const wxString& aPageNumber, const wxString& aPageName )
|
|
{
|
|
wxASSERT( m_outputFile );
|
|
|
|
// First things first: the customary null object
|
|
m_xrefTable.clear();
|
|
m_xrefTable.push_back( 0 );
|
|
m_hyperlinksInPage.clear();
|
|
m_hyperlinkMenusInPage.clear();
|
|
m_hyperlinkHandles.clear();
|
|
m_hyperlinkMenuHandles.clear();
|
|
m_bookmarksInPage.clear();
|
|
m_totalOutlineNodes = 0;
|
|
|
|
m_outlineRoot = std::make_unique<OUTLINE_NODE>();
|
|
|
|
if( !m_strokeFontManager )
|
|
m_strokeFontManager = std::make_unique<PDF_STROKE_FONT_MANAGER>();
|
|
else
|
|
m_strokeFontManager->Reset();
|
|
|
|
if( !m_outlineFontManager )
|
|
m_outlineFontManager = std::make_unique<PDF_OUTLINE_FONT_MANAGER>();
|
|
else
|
|
m_outlineFontManager->Reset();
|
|
|
|
/* The header (that's easy!). The second line is binary junk required
|
|
to make the file binary from the beginning (the important thing is
|
|
that they must have the bit 7 set) */
|
|
fmt::print( m_outputFile, "%PDF-1.5\n%\200\201\202\203\n" );
|
|
|
|
/* Allocate an entry for the page tree root, it will go in every page parent entry */
|
|
m_pageTreeHandle = allocPdfObject();
|
|
|
|
/* In the same way, the font resource dictionary is used by every page
|
|
(it *could* be inherited via the Pages tree */
|
|
m_fontResDictHandle = allocPdfObject();
|
|
|
|
m_imgResDictHandle = allocPdfObject();
|
|
|
|
m_jsNamesHandle = allocPdfObject();
|
|
|
|
/* Now, the PDF is read from the end, (more or less)... so we start
|
|
with the page stream for page 1. Other more important stuff is written
|
|
at the end */
|
|
StartPage( aPageNumber, aPageName );
|
|
return true;
|
|
}
|
|
|
|
|
|
int PDF_PLOTTER::emitGoToAction( int aPageHandle, const VECTOR2I& aBottomLeft,
|
|
const VECTOR2I& aTopRight )
|
|
{
|
|
int actionHandle = allocPdfObject();
|
|
startPdfObject( actionHandle );
|
|
|
|
fmt::print( m_outputFile,
|
|
"<</S /GoTo /D [{} 0 R /FitR {} {} {} {}]\n"
|
|
">>\n",
|
|
aPageHandle,
|
|
aBottomLeft.x,
|
|
aBottomLeft.y,
|
|
aTopRight.x,
|
|
aTopRight.y );
|
|
|
|
closePdfObject();
|
|
|
|
return actionHandle;
|
|
}
|
|
|
|
|
|
int PDF_PLOTTER::emitGoToAction( int aPageHandle )
|
|
{
|
|
int actionHandle = allocPdfObject();
|
|
startPdfObject( actionHandle );
|
|
|
|
fmt::println( m_outputFile,
|
|
"<</S /GoTo /D [{} 0 R /Fit]\n"
|
|
">>",
|
|
aPageHandle );
|
|
|
|
closePdfObject();
|
|
|
|
return actionHandle;
|
|
}
|
|
|
|
|
|
void PDF_PLOTTER::emitOutlineNode( OUTLINE_NODE* node, int parentHandle, int nextNode,
|
|
int prevNode )
|
|
{
|
|
int nodeHandle = node->entryHandle;
|
|
int prevHandle = -1;
|
|
int nextHandle = -1;
|
|
|
|
for( std::vector<OUTLINE_NODE*>::iterator it = node->children.begin();
|
|
it != node->children.end(); it++ )
|
|
{
|
|
if( it >= node->children.end() - 1 )
|
|
{
|
|
nextHandle = -1;
|
|
}
|
|
else
|
|
{
|
|
nextHandle = ( *( it + 1 ) )->entryHandle;
|
|
}
|
|
|
|
emitOutlineNode( *it, nodeHandle, nextHandle, prevHandle );
|
|
|
|
prevHandle = ( *it )->entryHandle;
|
|
}
|
|
|
|
// -1 for parentHandle is the outline root itself which is handed elsewhere.
|
|
if( parentHandle != -1 )
|
|
{
|
|
startPdfObject( nodeHandle );
|
|
|
|
fmt::print( m_outputFile,
|
|
"<<\n"
|
|
"/Title {}\n"
|
|
"/Parent {} 0 R\n",
|
|
encodeStringForPlotter(node->title ),
|
|
parentHandle);
|
|
|
|
if( nextNode > 0 )
|
|
{
|
|
fmt::println( m_outputFile, "/Next {} 0 R", nextNode );
|
|
}
|
|
|
|
if( prevNode > 0 )
|
|
{
|
|
fmt::println( m_outputFile, "/Prev {} 0 R", prevNode );
|
|
}
|
|
|
|
if( node->children.size() > 0 )
|
|
{
|
|
int32_t count = -1 * static_cast<int32_t>( node->children.size() );
|
|
fmt::println( m_outputFile, "/Count {}", count );
|
|
fmt::println( m_outputFile, "/First {} 0 R", node->children.front()->entryHandle );
|
|
fmt::println( m_outputFile, "/Last {} 0 R", node->children.back()->entryHandle );
|
|
}
|
|
|
|
if( node->actionHandle != -1 )
|
|
{
|
|
fmt::println( m_outputFile, "/A {} 0 R", node->actionHandle );
|
|
}
|
|
|
|
fmt::println( m_outputFile, ">>" );
|
|
closePdfObject();
|
|
}
|
|
}
|
|
|
|
|
|
PDF_PLOTTER::OUTLINE_NODE* PDF_PLOTTER::addOutlineNode( OUTLINE_NODE* aParent, int aActionHandle,
|
|
const wxString& aTitle )
|
|
{
|
|
OUTLINE_NODE *node = aParent->AddChild( aActionHandle, aTitle, allocPdfObject() );
|
|
m_totalOutlineNodes++;
|
|
|
|
return node;
|
|
}
|
|
|
|
|
|
int PDF_PLOTTER::emitOutline()
|
|
{
|
|
if( m_outlineRoot->children.size() > 0 )
|
|
{
|
|
// declare the outline object
|
|
m_outlineRoot->entryHandle = allocPdfObject();
|
|
|
|
emitOutlineNode( m_outlineRoot.get(), -1, -1, -1 );
|
|
|
|
startPdfObject( m_outlineRoot->entryHandle );
|
|
|
|
fmt::print( m_outputFile,
|
|
"<< /Type /Outlines\n"
|
|
" /Count {}\n"
|
|
" /First {} 0 R\n"
|
|
" /Last {} 0 R\n"
|
|
">>\n",
|
|
m_totalOutlineNodes,
|
|
m_outlineRoot->children.front()->entryHandle,
|
|
m_outlineRoot->children.back()->entryHandle
|
|
);
|
|
|
|
closePdfObject();
|
|
|
|
return m_outlineRoot->entryHandle;
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
void PDF_PLOTTER::emitStrokeFonts()
|
|
{
|
|
if( !m_strokeFontManager )
|
|
return;
|
|
|
|
for( PDF_STROKE_FONT_SUBSET* subsetPtr : m_strokeFontManager->AllSubsets() )
|
|
{
|
|
PDF_STROKE_FONT_SUBSET& subset = *subsetPtr;
|
|
|
|
if( subset.GlyphCount() <= 1 )
|
|
{
|
|
subset.SetCharProcsHandle( -1 );
|
|
subset.SetFontHandle( -1 );
|
|
subset.SetToUnicodeHandle( -1 );
|
|
continue;
|
|
}
|
|
|
|
for( PDF_STROKE_FONT_SUBSET::GLYPH& glyph : subset.Glyphs() )
|
|
{
|
|
int charProcHandle = startPdfStream();
|
|
|
|
if( !glyph.m_stream.empty() )
|
|
fmt::print( m_workFile, "{}\n", glyph.m_stream );
|
|
|
|
closePdfStream();
|
|
glyph.m_charProcHandle = charProcHandle;
|
|
}
|
|
|
|
int charProcDictHandle = startPdfObject();
|
|
fmt::println( m_outputFile, "<<" );
|
|
|
|
for( const PDF_STROKE_FONT_SUBSET::GLYPH& glyph : subset.Glyphs() )
|
|
fmt::println( m_outputFile, " /{} {} 0 R", glyph.m_name, glyph.m_charProcHandle );
|
|
|
|
fmt::println( m_outputFile, ">>" );
|
|
closePdfObject();
|
|
subset.SetCharProcsHandle( charProcDictHandle );
|
|
|
|
int toUnicodeHandle = startPdfStream();
|
|
std::string cmap = subset.BuildToUnicodeCMap();
|
|
|
|
if( !cmap.empty() )
|
|
fmt::print( m_workFile, "{}", cmap );
|
|
|
|
closePdfStream();
|
|
subset.SetToUnicodeHandle( toUnicodeHandle );
|
|
|
|
double fontMatrixScale = 1.0 / subset.UnitsPerEm();
|
|
double minX = subset.FontBBoxMinX();
|
|
double minY = subset.FontBBoxMinY();
|
|
double maxX = subset.FontBBoxMaxX();
|
|
double maxY = subset.FontBBoxMaxY();
|
|
|
|
int fontHandle = startPdfObject();
|
|
fmt::print( m_outputFile,
|
|
"<<\n/Type /Font\n/Subtype /Type3\n/Name {}\n/FontBBox [ {:g} {:g} {:g} {:g} ]\n",
|
|
subset.ResourceName(),
|
|
minX,
|
|
minY,
|
|
maxX,
|
|
maxY );
|
|
fmt::print( m_outputFile,
|
|
"/FontMatrix [ {:g} 0 0 {:g} 0 0 ]\n/CharProcs {} 0 R\n",
|
|
fontMatrixScale,
|
|
fontMatrixScale,
|
|
subset.CharProcsHandle() );
|
|
fmt::print( m_outputFile,
|
|
"/Encoding << /Type /Encoding /Differences {} >>\n",
|
|
subset.BuildDifferencesArray() );
|
|
fmt::print( m_outputFile,
|
|
"/FirstChar {}\n/LastChar {}\n/Widths {}\n",
|
|
subset.FirstChar(),
|
|
subset.LastChar(),
|
|
subset.BuildWidthsArray() );
|
|
fmt::print( m_outputFile,
|
|
"/ToUnicode {} 0 R\n/Resources << /ProcSet [/PDF /Text] >>\n>>\n",
|
|
subset.ToUnicodeHandle() );
|
|
closePdfObject();
|
|
subset.SetFontHandle( fontHandle );
|
|
}
|
|
}
|
|
|
|
|
|
void PDF_PLOTTER::emitOutlineFonts()
|
|
{
|
|
if( !m_outlineFontManager )
|
|
return;
|
|
|
|
for( PDF_OUTLINE_FONT_SUBSET* subsetPtr : m_outlineFontManager->AllSubsets() )
|
|
{
|
|
if( !subsetPtr || !subsetPtr->HasGlyphs() )
|
|
continue;
|
|
|
|
const std::vector<uint8_t>& fontData = subsetPtr->FontFileData();
|
|
|
|
if( fontData.empty() )
|
|
continue;
|
|
|
|
int fontFileHandle = startPdfStream();
|
|
subsetPtr->SetFontFileHandle( fontFileHandle );
|
|
|
|
if( !fontData.empty() )
|
|
fwrite( fontData.data(), fontData.size(), 1, m_workFile );
|
|
|
|
closePdfStream();
|
|
|
|
std::string cidMap = subsetPtr->BuildCIDToGIDStream();
|
|
int cidMapHandle = startPdfStream();
|
|
subsetPtr->SetCIDMapHandle( cidMapHandle );
|
|
|
|
if( !cidMap.empty() )
|
|
fwrite( cidMap.data(), cidMap.size(), 1, m_workFile );
|
|
|
|
closePdfStream();
|
|
|
|
std::string toUnicode = subsetPtr->BuildToUnicodeCMap();
|
|
int toUnicodeHandle = startPdfStream();
|
|
subsetPtr->SetToUnicodeHandle( toUnicodeHandle );
|
|
|
|
if( !toUnicode.empty() )
|
|
fmt::print( m_workFile, "{}", toUnicode );
|
|
|
|
closePdfStream();
|
|
|
|
int descriptorHandle = startPdfObject();
|
|
subsetPtr->SetFontDescriptorHandle( descriptorHandle );
|
|
|
|
fmt::print( m_outputFile,
|
|
"<<\n/Type /FontDescriptor\n/FontName /{}\n/Flags {}\n/ItalicAngle {:g}\n/Ascent {:g}\n/Descent {:g}\n"
|
|
"/CapHeight {:g}\n/StemV {:g}\n/FontBBox [ {:g} {:g} {:g} {:g} ]\n/FontFile2 {} 0 R\n>>\n",
|
|
subsetPtr->BaseFontName(),
|
|
subsetPtr->Flags(),
|
|
subsetPtr->ItalicAngle(),
|
|
subsetPtr->Ascent(),
|
|
subsetPtr->Descent(),
|
|
subsetPtr->CapHeight(),
|
|
subsetPtr->StemV(),
|
|
subsetPtr->BBoxMinX(),
|
|
subsetPtr->BBoxMinY(),
|
|
subsetPtr->BBoxMaxX(),
|
|
subsetPtr->BBoxMaxY(),
|
|
subsetPtr->FontFileHandle() );
|
|
closePdfObject();
|
|
|
|
int cidFontHandle = startPdfObject();
|
|
subsetPtr->SetCIDFontHandle( cidFontHandle );
|
|
|
|
std::string widths = subsetPtr->BuildWidthsArray();
|
|
|
|
fmt::print( m_outputFile,
|
|
"<<\n/Type /Font\n/Subtype /CIDFontType2\n/BaseFont /{}\n"
|
|
"/CIDSystemInfo << /Registry (Adobe) /Ordering (Identity) /Supplement 0 >>\n"
|
|
"/FontDescriptor {} 0 R\n/W {}\n/CIDToGIDMap {} 0 R\n>>\n",
|
|
subsetPtr->BaseFontName(),
|
|
subsetPtr->FontDescriptorHandle(),
|
|
widths,
|
|
subsetPtr->CIDMapHandle() );
|
|
closePdfObject();
|
|
|
|
int fontHandle = startPdfObject();
|
|
subsetPtr->SetFontHandle( fontHandle );
|
|
|
|
fmt::print( m_outputFile,
|
|
"<<\n/Type /Font\n/Subtype /Type0\n/BaseFont /{}\n/Encoding /Identity-H\n"
|
|
"/DescendantFonts [ {} 0 R ]\n/ToUnicode {} 0 R\n>>\n",
|
|
subsetPtr->BaseFontName(),
|
|
subsetPtr->CIDFontHandle(),
|
|
subsetPtr->ToUnicodeHandle() );
|
|
closePdfObject();
|
|
}
|
|
}
|
|
|
|
|
|
void PDF_PLOTTER::endPlotEmitResources()
|
|
{
|
|
emitOutlineFonts();
|
|
emitStrokeFonts();
|
|
|
|
startPdfObject( m_fontResDictHandle );
|
|
fmt::println( m_outputFile, "<<" );
|
|
|
|
if( m_outlineFontManager )
|
|
{
|
|
for( PDF_OUTLINE_FONT_SUBSET* subsetPtr : m_outlineFontManager->AllSubsets() )
|
|
{
|
|
if( subsetPtr && subsetPtr->FontHandle() >= 0 )
|
|
fmt::println( m_outputFile, " {} {} 0 R", subsetPtr->ResourceName(), subsetPtr->FontHandle() );
|
|
}
|
|
}
|
|
|
|
if( m_strokeFontManager )
|
|
{
|
|
for( PDF_STROKE_FONT_SUBSET* subsetPtr : m_strokeFontManager->AllSubsets() )
|
|
{
|
|
const PDF_STROKE_FONT_SUBSET& subset = *subsetPtr;
|
|
if( subset.FontHandle() >= 0 )
|
|
fmt::println( m_outputFile, " {} {} 0 R", subset.ResourceName(), subset.FontHandle() );
|
|
}
|
|
}
|
|
|
|
fmt::println( m_outputFile, ">>" );
|
|
closePdfObject();
|
|
|
|
// Named image dictionary (was allocated, now we emit it)
|
|
startPdfObject( m_imgResDictHandle );
|
|
fmt::println( m_outputFile, "<<\n" );
|
|
|
|
for( const auto& [imgHandle, image] : m_imageHandles )
|
|
{
|
|
fmt::print( m_outputFile, " /Im{} {} 0 R\n", imgHandle, imgHandle );
|
|
}
|
|
|
|
fmt::println( m_outputFile, ">>" );
|
|
closePdfObject();
|
|
|
|
// Emit images with optional SMask for transparency
|
|
for( const auto& [imgHandle, image] : m_imageHandles )
|
|
{
|
|
// Init wxFFile so wxFFileOutputStream won't close file in dtor.
|
|
wxFFile outputFFile( m_outputFile );
|
|
|
|
// Image
|
|
startPdfObject( imgHandle );
|
|
int imgLenHandle = allocPdfObject();
|
|
int smaskHandle = ( image.HasAlpha() || image.HasMask() ) ? allocPdfObject() : -1;
|
|
|
|
fmt::print( m_outputFile,
|
|
"<<\n"
|
|
"/Type /XObject\n"
|
|
"/Subtype /Image\n"
|
|
"/BitsPerComponent 8\n"
|
|
"/ColorSpace {}\n"
|
|
"/Width {}\n"
|
|
"/Height {}\n"
|
|
"/Filter /FlateDecode\n"
|
|
"/Length {} 0 R\n", // Length is deferred
|
|
m_colorMode ? "/DeviceRGB" : "/DeviceGray",
|
|
image.GetWidth(),
|
|
image.GetHeight(),
|
|
imgLenHandle );
|
|
|
|
if( smaskHandle != -1 )
|
|
fmt::println( m_outputFile, "/SMask {} 0 R", smaskHandle );
|
|
|
|
fmt::println( m_outputFile, ">>" );
|
|
fmt::println( m_outputFile, "stream" );
|
|
|
|
long imgStreamStart = ftell( m_outputFile );
|
|
|
|
{
|
|
wxFFileOutputStream ffos( outputFFile );
|
|
wxZlibOutputStream zos( ffos, wxZ_BEST_COMPRESSION, wxZLIB_ZLIB );
|
|
wxDataOutputStream dos( zos );
|
|
|
|
WriteImageStream( image, dos, m_renderSettings->GetBackgroundColor().ToColour(),
|
|
m_colorMode );
|
|
}
|
|
|
|
long imgStreamSize = ftell( m_outputFile ) - imgStreamStart;
|
|
|
|
fmt::print( m_outputFile, "\nendstream\n" );
|
|
closePdfObject();
|
|
|
|
startPdfObject( imgLenHandle );
|
|
fmt::println( m_outputFile, "{}", imgStreamSize );
|
|
closePdfObject();
|
|
|
|
if( smaskHandle != -1 )
|
|
{
|
|
// SMask
|
|
startPdfObject( smaskHandle );
|
|
int smaskLenHandle = allocPdfObject();
|
|
|
|
fmt::print( m_outputFile,
|
|
"<<\n"
|
|
"/Type /XObject\n"
|
|
"/Subtype /Image\n"
|
|
"/BitsPerComponent 8\n"
|
|
"/ColorSpace /DeviceGray\n"
|
|
"/Width {}\n"
|
|
"/Height {}\n"
|
|
"/Length {} 0 R\n"
|
|
"/Filter /FlateDecode\n"
|
|
">>\n", // Length is deferred
|
|
image.GetWidth(),
|
|
image.GetHeight(),
|
|
smaskLenHandle );
|
|
|
|
fmt::println( m_outputFile, "stream" );
|
|
|
|
long smaskStreamStart = ftell( m_outputFile );
|
|
|
|
{
|
|
wxFFileOutputStream ffos( outputFFile );
|
|
wxZlibOutputStream zos( ffos, wxZ_BEST_COMPRESSION, wxZLIB_ZLIB );
|
|
wxDataOutputStream dos( zos );
|
|
|
|
WriteImageSMaskStream( image, dos );
|
|
}
|
|
|
|
long smaskStreamSize = ftell( m_outputFile ) - smaskStreamStart;
|
|
|
|
fmt::print( m_outputFile, "\nendstream\n" );
|
|
closePdfObject();
|
|
|
|
startPdfObject( smaskLenHandle );
|
|
fmt::println( m_outputFile, "{}", (unsigned) smaskStreamSize );
|
|
closePdfObject();
|
|
}
|
|
|
|
outputFFile.Detach(); // Don't close it
|
|
}
|
|
|
|
for( const auto& [ linkHandle, linkPair ] : m_hyperlinkHandles )
|
|
{
|
|
BOX2D box = linkPair.first;
|
|
wxString url = linkPair.second;
|
|
|
|
startPdfObject( linkHandle );
|
|
|
|
fmt::print( m_outputFile,
|
|
"<<\n"
|
|
"/Type /Annot\n"
|
|
"/Subtype /Link\n"
|
|
"/Rect [{:g} {:g} {:g} {:g}]\n"
|
|
"/Border [16 16 0]\n",
|
|
box.GetLeft(),
|
|
box.GetBottom(),
|
|
box.GetRight(),
|
|
box.GetTop() );
|
|
|
|
wxString pageNumber;
|
|
bool pageFound = false;
|
|
|
|
if( EDA_TEXT::IsGotoPageHref( url, &pageNumber ) )
|
|
{
|
|
for( size_t ii = 0; ii < m_pageNumbers.size(); ++ii )
|
|
{
|
|
if( m_pageNumbers[ii] == pageNumber )
|
|
{
|
|
fmt::print( m_outputFile,
|
|
"/Dest [{} 0 R /FitB]\n"
|
|
">>\n",
|
|
m_pageHandles[ii] );
|
|
|
|
pageFound = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if( !pageFound )
|
|
{
|
|
// destination page is not being plotted, assign the NOP action to the link
|
|
fmt::print( m_outputFile, "/A << /Type /Action /S /NOP >>\n"
|
|
">>\n" );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if( m_project )
|
|
url = ResolveUriByEnvVars( url, m_project );
|
|
|
|
fmt::print( m_outputFile,
|
|
"/A << /Type /Action /S /URI /URI {} >>\n"
|
|
">>\n",
|
|
encodeStringForPlotter( url ) );
|
|
}
|
|
|
|
closePdfObject();
|
|
}
|
|
|
|
for( const auto& [ menuHandle, menuPair ] : m_hyperlinkMenuHandles )
|
|
{
|
|
const BOX2D& box = menuPair.first;
|
|
const std::vector<wxString>& urls = menuPair.second;
|
|
wxString js = wxT( "ShM([\n" );
|
|
|
|
for( const wxString& url : urls )
|
|
{
|
|
if( url.StartsWith( "!" ) )
|
|
{
|
|
wxString property = url.AfterFirst( '!' );
|
|
|
|
if( property.Find( "http:" ) >= 0 )
|
|
{
|
|
wxString href = property.substr( property.Find( "http:" ) );
|
|
|
|
if( m_project )
|
|
href = ResolveUriByEnvVars( href, m_project );
|
|
|
|
js += wxString::Format( wxT( "[\"%s\", \"%s\"],\n" ),
|
|
EscapeString( href, CTX_JS_STR ),
|
|
EscapeString( href, CTX_JS_STR ) );
|
|
}
|
|
else if( property.Find( "https:" ) >= 0 )
|
|
{
|
|
wxString href = property.substr( property.Find( "https:" ) );
|
|
|
|
if( m_project )
|
|
href = ResolveUriByEnvVars( href, m_project );
|
|
|
|
js += wxString::Format( wxT( "[\"%s\", \"%s\"],\n" ),
|
|
EscapeString( href, CTX_JS_STR ),
|
|
EscapeString( href, CTX_JS_STR ) );
|
|
}
|
|
else if( property.Find( "file:" ) >= 0 )
|
|
{
|
|
wxString href = property.substr( property.Find( "file:" ) );
|
|
|
|
if( m_project )
|
|
href = ResolveUriByEnvVars( href, m_project );
|
|
|
|
href = NormalizeFileUri( href );
|
|
|
|
js += wxString::Format( wxT( "[\"%s\", \"%s\"],\n" ),
|
|
EscapeString( href, CTX_JS_STR ),
|
|
EscapeString( href, CTX_JS_STR ) );
|
|
}
|
|
else
|
|
{
|
|
js += wxString::Format( wxT( "[\"%s\"],\n" ),
|
|
EscapeString( property, CTX_JS_STR ) );
|
|
}
|
|
}
|
|
else if( url.StartsWith( "#" ) )
|
|
{
|
|
wxString pageNumber = url.AfterFirst( '#' );
|
|
|
|
for( size_t ii = 0; ii < m_pageNumbers.size(); ++ii )
|
|
{
|
|
if( m_pageNumbers[ii] == pageNumber )
|
|
{
|
|
wxString menuText = wxString::Format( _( "Show Page %s" ), pageNumber );
|
|
|
|
js += wxString::Format( wxT( "[\"%s\", \"#%d\"],\n" ),
|
|
EscapeString( menuText, CTX_JS_STR ),
|
|
static_cast<int>( ii ) );
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else if( url.StartsWith( "http:" ) || url.StartsWith( "https:" )
|
|
|| url.StartsWith( "file:" ) )
|
|
{
|
|
wxString href = url;
|
|
|
|
if( m_project )
|
|
href = ResolveUriByEnvVars( url, m_project );
|
|
|
|
if( url.StartsWith( "file:" ) )
|
|
href = NormalizeFileUri( href );
|
|
|
|
wxString menuText = wxString::Format( _( "Open %s" ), href );
|
|
|
|
js += wxString::Format( wxT( "[\"%s\", \"%s\"],\n" ),
|
|
EscapeString( href, CTX_JS_STR ),
|
|
EscapeString( href, CTX_JS_STR ) );
|
|
}
|
|
}
|
|
|
|
js += wxT( "]);" );
|
|
|
|
startPdfObject( menuHandle );
|
|
|
|
fmt::print( m_outputFile,
|
|
"<<\n"
|
|
"/Type /Annot\n"
|
|
"/Subtype /Link\n"
|
|
"/Rect [{:g} {:g} {:g} {:g}]\n"
|
|
"/Border [16 16 0]\n",
|
|
box.GetLeft(), box.GetBottom(), box.GetRight(), box.GetTop() );
|
|
|
|
fmt::print( m_outputFile,
|
|
"/A << /Type /Action /S /JavaScript /JS {} >>\n"
|
|
">>\n",
|
|
encodeStringForPlotter( js ) );
|
|
|
|
closePdfObject();
|
|
}
|
|
|
|
{
|
|
startPdfObject( m_jsNamesHandle );
|
|
|
|
wxString js = R"JS(
|
|
function ShM(aEntries) {
|
|
var aParams = [];
|
|
for (var i in aEntries) {
|
|
aParams.push({
|
|
cName: aEntries[i][0],
|
|
cReturn: aEntries[i][1]
|
|
})
|
|
}
|
|
|
|
var cChoice = app.popUpMenuEx.apply(app, aParams);
|
|
if (cChoice != null && cChoice.substring(0, 1) == '#') this.pageNum = parseInt(cChoice.slice(1));
|
|
else if (cChoice != null && cChoice.substring(0, 4) == 'http') app.launchURL(cChoice);
|
|
else if (cChoice != null && cChoice.substring(0, 4) == 'file') app.openDoc(cChoice.substring(7));
|
|
}
|
|
)JS";
|
|
|
|
fmt::print( m_outputFile,
|
|
"<< /JavaScript\n"
|
|
" << /Names\n"
|
|
" [ (JSInit) << /Type /Action /S /JavaScript /JS {} >> ]\n"
|
|
" >>\n"
|
|
">>\n",
|
|
encodeStringForPlotter( js ) );
|
|
|
|
closePdfObject();
|
|
}
|
|
}
|
|
|
|
bool PDF_PLOTTER::EndPlot()
|
|
{
|
|
// We can end up here if there was nothing to plot
|
|
if( !m_outputFile )
|
|
return false;
|
|
|
|
// Close the current page (often the only one)
|
|
ClosePage();
|
|
|
|
if( !m_3dExportMode )
|
|
{
|
|
endPlotEmitResources();
|
|
}
|
|
|
|
/* The page tree: it's a B-tree but luckily we only have few pages!
|
|
So we use just an array... The handle was allocated at the beginning,
|
|
now we instantiate the corresponding object */
|
|
startPdfObject( m_pageTreeHandle );
|
|
fmt::print( m_outputFile,
|
|
"<<\n"
|
|
"/Type /Pages\n"
|
|
"/Kids [\n" );
|
|
|
|
for( unsigned i = 0; i < m_pageHandles.size(); i++ )
|
|
fmt::println( m_outputFile, "{} 0 R", m_pageHandles[i] );
|
|
|
|
fmt::print( m_outputFile,
|
|
"]\n"
|
|
"/Count {}\n"
|
|
">>\n", m_pageHandles.size() );
|
|
closePdfObject();
|
|
|
|
int infoDictHandle = startPdfObject();
|
|
|
|
std::string dt = fmt::format( "D:{:%Y:%m:%d:%H:%M:%S}", fmt::localtime( std::time( nullptr ) ) );
|
|
|
|
if( m_title.IsEmpty() )
|
|
{
|
|
// Windows uses '\' and other platforms use '/' as separator
|
|
m_title = m_filename.AfterLast( '\\' );
|
|
m_title = m_title.AfterLast( '/' );
|
|
}
|
|
|
|
fmt::print( m_outputFile,
|
|
"<<\n"
|
|
"/Producer (KiCad PDF)\n"
|
|
"/CreationDate ({})\n"
|
|
"/Creator {}\n"
|
|
"/Title {}\n"
|
|
"/Author {}\n"
|
|
"/Subject {}\n",
|
|
dt,
|
|
encodeStringForPlotter( m_creator ),
|
|
encodeStringForPlotter( m_title ),
|
|
encodeStringForPlotter( m_author ),
|
|
encodeStringForPlotter( m_subject ) );
|
|
|
|
fmt::println( m_outputFile, ">>" );
|
|
closePdfObject();
|
|
|
|
// Let's dump in the outline
|
|
int outlineHandle = -1;
|
|
if( !m_3dExportMode )
|
|
{
|
|
outlineHandle = emitOutline();
|
|
}
|
|
|
|
// The catalog, at last
|
|
int catalogHandle = startPdfObject();
|
|
|
|
if( outlineHandle > 0 )
|
|
{
|
|
fmt::println( m_outputFile,
|
|
"<<\n"
|
|
"/Type /Catalog\n"
|
|
"/Pages {} 0 R\n"
|
|
"/Version /1.5\n"
|
|
"/PageMode /UseOutlines\n"
|
|
"/Outlines {} 0 R\n"
|
|
"/Names {} 0 R\n"
|
|
"/PageLayout /SinglePage\n"
|
|
">>",
|
|
m_pageTreeHandle,
|
|
outlineHandle,
|
|
m_jsNamesHandle );
|
|
}
|
|
else
|
|
{
|
|
fmt::println( m_outputFile,
|
|
"<<\n"
|
|
"/Type /Catalog\n"
|
|
"/Pages {} 0 R\n"
|
|
"/Version /1.5\n"
|
|
"/PageMode /UseNone\n"
|
|
"/PageLayout /SinglePage\n"
|
|
">>",
|
|
m_pageTreeHandle );
|
|
}
|
|
|
|
closePdfObject();
|
|
|
|
/* Emit the xref table (format is crucial to the byte, each entry must
|
|
be 20 bytes long, and object zero must be done in that way). Also
|
|
the offset must be kept along for the trailer */
|
|
long xref_start = ftell( m_outputFile );
|
|
fmt::print( m_outputFile,
|
|
"xref\n"
|
|
"0 {}\n"
|
|
"0000000000 65535 f \n",
|
|
m_xrefTable.size() );
|
|
|
|
for( unsigned i = 1; i < m_xrefTable.size(); i++ )
|
|
{
|
|
fmt::print( m_outputFile, "{:010d} 00000 n \n", m_xrefTable[i] );
|
|
}
|
|
|
|
// Done the xref, go for the trailer
|
|
fmt::print( m_outputFile,
|
|
"trailer\n"
|
|
"<< /Size {} /Root {} 0 R /Info {} 0 R >>\n"
|
|
"startxref\n"
|
|
"{}\n" // The offset we saved before
|
|
"%%EOF\n",
|
|
m_xrefTable.size(),
|
|
catalogHandle,
|
|
infoDictHandle,
|
|
xref_start );
|
|
|
|
fclose( m_outputFile );
|
|
m_outputFile = nullptr;
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
struct OverbarInfo
|
|
{
|
|
VECTOR2I startPos;
|
|
VECTOR2I endPos;
|
|
VECTOR2I fontSize;
|
|
};
|
|
|
|
|
|
void PDF_PLOTTER::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 )
|
|
{
|
|
// PDF files do not like 0 sized texts which create broken files.
|
|
if( aSize.x == 0 || aSize.y == 0 )
|
|
return;
|
|
|
|
wxString text( aText );
|
|
|
|
if( text.Contains( wxS( "@{" ) ) )
|
|
{
|
|
EXPRESSION_EVALUATOR evaluator;
|
|
text = evaluator.Evaluate( text );
|
|
}
|
|
|
|
SetColor( aColor );
|
|
SetCurrentLineWidth( aWidth, aData );
|
|
|
|
if( !aFont )
|
|
aFont = KIFONT::FONT::GetFont( m_renderSettings->GetDefaultFont() );
|
|
|
|
VECTOR2I t_size( std::abs( aSize.x ), std::abs( aSize.y ) );
|
|
bool textMirrored = aSize.x < 0;
|
|
|
|
// Parse the text for markup
|
|
MARKUP::MARKUP_PARSER markupParser( text.ToStdString() );
|
|
std::unique_ptr<MARKUP::NODE> markupTree( markupParser.Parse() );
|
|
|
|
if( !markupTree )
|
|
{
|
|
wxLogTrace( tracePdfPlotter, "PDF_PLOTTER::Text: Markup parsing failed, falling back to plain text." );
|
|
// Fallback to simple text rendering if parsing fails
|
|
wxStringTokenizer str_tok( text, " ", wxTOKEN_RET_DELIMS );
|
|
VECTOR2I pos = aPos;
|
|
|
|
while( str_tok.HasMoreTokens() )
|
|
{
|
|
wxString word = str_tok.GetNextToken();
|
|
pos = renderWord( word, pos, t_size, aOrient, textMirrored, aWidth, aBold, aItalic,
|
|
aFont, aFontMetrics, aV_justify, 0 );
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Calculate the full text bounding box for alignment
|
|
VECTOR2I full_box( aFont->StringBoundaryLimits( text, t_size, aWidth, aBold, aItalic,
|
|
aFontMetrics ) );
|
|
|
|
if( textMirrored )
|
|
full_box.x *= -1;
|
|
|
|
VECTOR2I box_x( full_box.x, 0 );
|
|
VECTOR2I box_y( 0, full_box.y );
|
|
|
|
RotatePoint( box_x, aOrient );
|
|
RotatePoint( box_y, aOrient );
|
|
|
|
VECTOR2I pos( aPos );
|
|
if( aH_justify == GR_TEXT_H_ALIGN_CENTER )
|
|
pos -= box_x / 2;
|
|
else if( aH_justify == GR_TEXT_H_ALIGN_RIGHT )
|
|
pos -= box_x;
|
|
|
|
if( aV_justify == GR_TEXT_V_ALIGN_CENTER )
|
|
pos += box_y / 2;
|
|
else if( aV_justify == GR_TEXT_V_ALIGN_TOP )
|
|
pos += box_y;
|
|
|
|
// Render markup tree
|
|
std::vector<OverbarInfo> overbars;
|
|
renderMarkupNode( markupTree.get(), pos, t_size, aOrient, textMirrored, aWidth,
|
|
aBold, aItalic, aFont, aFontMetrics, aV_justify, 0, overbars );
|
|
|
|
// Draw any overbars that were accumulated
|
|
drawOverbars( overbars, aOrient, aFontMetrics );
|
|
}
|
|
|
|
|
|
VECTOR2I PDF_PLOTTER::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 )
|
|
{
|
|
if( wxGetEnv( "KICAD_DEBUG_SYN_STYLE", nullptr ) )
|
|
{
|
|
int styleFlags = 0;
|
|
if( aFont && aFont->IsOutline() )
|
|
styleFlags = static_cast<KIFONT::OUTLINE_FONT*>( aFont )->GetFace() ? static_cast<KIFONT::OUTLINE_FONT*>( aFont )->GetFace()->style_flags : 0;
|
|
wxLogTrace( tracePdfPlotter, "renderWord enter word='%s' bold=%d italic=%d textStyle=%u styleFlags=%d", TO_UTF8( aWord ), (int) aBold, (int) aItalic, (unsigned) aTextStyle, styleFlags );
|
|
}
|
|
|
|
// Don't try to output a blank string, but handle space characters for word separation
|
|
if( aWord.empty() )
|
|
return aPosition;
|
|
|
|
// If the word is just a space character, advance position by space width and continue
|
|
if( aWord == wxT(" ") )
|
|
{
|
|
// Calculate space width and advance position
|
|
VECTOR2I spaceBox( aFont->StringBoundaryLimits( wxT("n"), aSize, aWidth,
|
|
aBold, aItalic, aFontMetrics ).x / 2, 0 );
|
|
|
|
if( aTextMirrored )
|
|
spaceBox.x *= -1;
|
|
|
|
VECTOR2I rotatedSpaceBox = spaceBox;
|
|
RotatePoint( rotatedSpaceBox, aOrient );
|
|
return aPosition + rotatedSpaceBox;
|
|
}
|
|
|
|
// Compute transformation parameters for this word
|
|
double ctm_a, ctm_b, ctm_c, ctm_d, ctm_e, ctm_f;
|
|
double wideningFactor, heightFactor;
|
|
|
|
computeTextParameters( aPosition, aWord, aOrient, aSize, aTextMirrored,
|
|
GR_TEXT_H_ALIGN_LEFT, GR_TEXT_V_ALIGN_BOTTOM, aWidth,
|
|
aItalic, aBold, &wideningFactor, &ctm_a, &ctm_b, &ctm_c,
|
|
&ctm_d, &ctm_e, &ctm_f, &heightFactor );
|
|
|
|
// Calculate next position for word spacing
|
|
VECTOR2I bbox( aFont->StringBoundaryLimits( aWord, aSize, aWidth, aBold, aItalic, aFontMetrics ).x, 0 );
|
|
if( aTextMirrored )
|
|
bbox.x *= -1;
|
|
RotatePoint( bbox, aOrient );
|
|
VECTOR2I nextPos = aPosition + bbox;
|
|
|
|
// Apply vertical offset for subscript/superscript
|
|
// Stroke font positioning (baseline) already correct per user feedback.
|
|
// Outline fonts need: superscript +1 full font height higher; subscript +1 full font height higher
|
|
if( aTextStyle & TEXT_STYLE::SUPERSCRIPT )
|
|
{
|
|
double factor = ( aFont && aFont->IsOutline() ) ? 0.050 : 0.030; // stroke original ~0.40, outline needs +1.0
|
|
VECTOR2I offset( 0, static_cast<int>( std::lround( aSize.y * factor ) ) );
|
|
RotatePoint( offset, aOrient );
|
|
ctm_e -= offset.x;
|
|
ctm_f += offset.y; // Note: PDF Y increases upward
|
|
}
|
|
else if( aTextStyle & TEXT_STYLE::SUBSCRIPT )
|
|
{
|
|
// For outline fonts raise by one font height versus stroke (which shifts downward slightly)
|
|
VECTOR2I offset( 0, 0 );
|
|
|
|
if( !aFont || aFont->IsStroke() )
|
|
offset.y = static_cast<int>( std::lround( aSize.y * 0.01 ) );
|
|
|
|
RotatePoint( offset, aOrient );
|
|
ctm_e += offset.x;
|
|
ctm_f -= offset.y; // Note: PDF Y increases upward
|
|
}
|
|
|
|
// Render the word using existing outline font logic
|
|
bool useOutlineFont = aFont && aFont->IsOutline();
|
|
|
|
if( useOutlineFont )
|
|
{
|
|
std::vector<PDF_OUTLINE_FONT_RUN> outlineRuns;
|
|
|
|
if( m_outlineFontManager )
|
|
{
|
|
m_outlineFontManager->EncodeString( aWord, static_cast<KIFONT::OUTLINE_FONT*>( aFont ),
|
|
( aItalic || ( aTextStyle & TEXT_STYLE::ITALIC ) ),
|
|
( aBold || ( aTextStyle & TEXT_STYLE::BOLD ) ),
|
|
&outlineRuns );
|
|
}
|
|
|
|
if( !outlineRuns.empty() )
|
|
{
|
|
// Apply baseline adjustment (keeping existing logic)
|
|
double baseline_factor = 0.17;
|
|
double alignment_multiplier = 1.0;
|
|
if( aV_justify == GR_TEXT_V_ALIGN_CENTER )
|
|
alignment_multiplier = 2.0;
|
|
else if( aV_justify == GR_TEXT_V_ALIGN_TOP )
|
|
alignment_multiplier = 4.0;
|
|
|
|
VECTOR2D font_size_dev = userToDeviceSize( aSize );
|
|
double baseline_adjustment = font_size_dev.y * baseline_factor * alignment_multiplier;
|
|
|
|
double adjusted_ctm_e = ctm_e;
|
|
double adjusted_ctm_f = ctm_f;
|
|
|
|
double angle_rad = aOrient.AsRadians();
|
|
double cos_angle = cos( angle_rad );
|
|
double sin_angle = sin( angle_rad );
|
|
|
|
adjusted_ctm_e = ctm_e - baseline_adjustment * sin_angle;
|
|
adjusted_ctm_f = ctm_f + baseline_adjustment * cos_angle;
|
|
|
|
double adj_c = ctm_c;
|
|
double adj_d = ctm_d;
|
|
|
|
if( aItalic && ( !aFont || !aFont->IsOutline() ) )
|
|
{
|
|
double tilt = -ITALIC_TILT;
|
|
if( wideningFactor < 0 )
|
|
tilt = -tilt;
|
|
|
|
adj_c -= ctm_a * tilt;
|
|
adj_d -= ctm_b * tilt;
|
|
}
|
|
|
|
// Synthetic italic (shear) for outline font if requested but font not intrinsically italic
|
|
bool syntheticItalicApplied = false;
|
|
double appliedTilt = 0.0;
|
|
double syn_c = adj_c;
|
|
double syn_d = adj_d;
|
|
double syn_a = ctm_a;
|
|
double syn_b = ctm_b;
|
|
bool wantItalic = ( aItalic || ( aTextStyle & TEXT_STYLE::ITALIC ) );
|
|
if( std::getenv( "KICAD_FORCE_SYN_ITALIC" ) )
|
|
wantItalic = true; // debug: ensure path triggers when forcing synthetic italic
|
|
bool wantBold = ( aBold || ( aTextStyle & TEXT_STYLE::BOLD ) );
|
|
bool fontIsItalic = aFont && aFont->IsOutline() && aFont->IsItalic();
|
|
bool fontIsBold = aFont && aFont->IsOutline() && aFont->IsBold();
|
|
bool fontIsFakeItalic = aFont && aFont->IsOutline() && static_cast<KIFONT::OUTLINE_FONT*>( aFont )->IsFakeItalic();
|
|
bool fontIsFakeBold = aFont && aFont->IsOutline() && static_cast<KIFONT::OUTLINE_FONT*>( aFont )->IsFakeBold();
|
|
|
|
// Environment overrides for testing synthetic italics:
|
|
// KICAD_FORCE_SYN_ITALIC=1 forces synthetic shear even if font has italic face
|
|
// KICAD_SYN_ITALIC_TILT=<float degrees or tangent?>: if value contains 'deg' treat as degrees,
|
|
// otherwise treat as raw tilt factor (x += tilt*y)
|
|
bool forceSynItalic = false;
|
|
double overrideTilt = 0.0;
|
|
if( const char* envForce = std::getenv( "KICAD_FORCE_SYN_ITALIC" ) )
|
|
{
|
|
if( *envForce != '\0' && *envForce != '0' )
|
|
forceSynItalic = true;
|
|
}
|
|
|
|
if( const char* envTilt = std::getenv( "KICAD_SYN_ITALIC_TILT" ) )
|
|
{
|
|
std::string tiltStr( envTilt );
|
|
try
|
|
{
|
|
if( tiltStr.find( "deg" ) != std::string::npos )
|
|
{
|
|
double deg = std::stod( tiltStr );
|
|
overrideTilt = tan( deg * M_PI / 180.0 );
|
|
}
|
|
else
|
|
{
|
|
overrideTilt = std::stod( tiltStr );
|
|
}
|
|
}
|
|
catch( ... )
|
|
{
|
|
overrideTilt = 0.0; // ignore malformed
|
|
}
|
|
}
|
|
|
|
// Trace after we know style flags
|
|
wxLogTrace( tracePdfPlotter,
|
|
"Outline path word='%s' runs=%zu wantItalic=%d fontIsItalic=%d fontIsFakeItalic=%d wantBold=%d fontIsBold=%d fontIsFakeBold=%d forceSyn=%d",
|
|
TO_UTF8( aWord ), outlineRuns.size(), (int) wantItalic, (int) fontIsItalic,
|
|
(int) fontIsFakeItalic, (int) wantBold, (int) fontIsBold, (int) fontIsFakeBold,
|
|
(int) forceSynItalic );
|
|
|
|
// Apply synthetic italic if:
|
|
// - Italic requested AND outline font
|
|
// - And either forceSynItalic env var set OR there is no REAL italic face.
|
|
// (A fake italic flag from fontconfig substitution should NOT block synthetic shear.)
|
|
bool realItalicFace = aFont && aFont->IsOutline() && aFont->IsItalic() && !fontIsFakeItalic;
|
|
if( wantItalic && aFont && aFont->IsOutline() && ( forceSynItalic || !realItalicFace ) )
|
|
{
|
|
// We want to apply a horizontal shear so that x' = x + tilt * y in the glyph's
|
|
// local coordinate system BEFORE rotation. The existing text matrix columns are:
|
|
// first column = (a, b)^T -> x-axis direction & scale
|
|
// second column = (c, d)^T -> y-axis direction & scale
|
|
// Prepending a shear matrix S = [[1 tilt][0 1]] (i.e. T' = T * S is WRONG here).
|
|
// We need to LEFT-multiply: T' = R * S where R is the original rotation/scale.
|
|
// Left multiplication keeps first column unchanged and adds (tilt * firstColumn)
|
|
// to the second column: (c', d') = (c + tilt * a, d + tilt * b).
|
|
// This produces a right-leaning italic for positive tilt.
|
|
double tilt = ( overrideTilt != 0.0 ) ? overrideTilt : ITALIC_TILT;
|
|
if( wideningFactor < 0 ) // mirrored text should mirror the shear
|
|
tilt = -tilt;
|
|
|
|
syn_c = adj_c + tilt * syn_a;
|
|
syn_d = adj_d + tilt * syn_b;
|
|
appliedTilt = tilt;
|
|
syntheticItalicApplied = true;
|
|
|
|
wxLogTrace( tracePdfPlotter,
|
|
"Synthetic italic shear applied: tilt=%f a=%f b=%f c->%f d->%f",
|
|
tilt, syn_a, syn_b, syn_c, syn_d );
|
|
}
|
|
|
|
if( wantBold && aFont && aFont->IsOutline() && !fontIsBold )
|
|
{
|
|
// Slight horizontal widening to simulate bold (~3%)
|
|
syn_a *= 1.03;
|
|
syn_b *= 1.03;
|
|
}
|
|
|
|
if( syntheticItalicApplied )
|
|
{
|
|
// PDF comment to allow manual inspection in the output stream
|
|
fmt::print( m_workFile, "% syn-italic tilt={} a={} b={} c={} d={}\n", appliedTilt, syn_a, syn_b, syn_c, syn_d );
|
|
}
|
|
|
|
fmt::print( m_workFile, "q {:f} {:f} {:f} {:f} {:f} {:f} cm BT {} Tr {:g} Tz ",
|
|
syn_a, syn_b, syn_c, syn_d, adjusted_ctm_e, adjusted_ctm_f,
|
|
0, // render_mode
|
|
wideningFactor * 100 );
|
|
|
|
for( const PDF_OUTLINE_FONT_RUN& run : outlineRuns )
|
|
{
|
|
fmt::print( m_workFile, "{} {:g} Tf <", run.m_subset->ResourceName(), heightFactor );
|
|
|
|
for( const PDF_OUTLINE_FONT_GLYPH& glyph : run.m_glyphs )
|
|
{
|
|
fmt::print( m_workFile, "{:02X}{:02X}",
|
|
static_cast<unsigned char>( ( glyph.cid >> 8 ) & 0xFF ),
|
|
static_cast<unsigned char>( glyph.cid & 0xFF ) );
|
|
}
|
|
|
|
fmt::print( m_workFile, "> Tj " );
|
|
}
|
|
|
|
fmt::println( m_workFile, "ET" );
|
|
fmt::println( m_workFile, "Q" );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Handle stroke fonts
|
|
if( !m_strokeFontManager )
|
|
return nextPos;
|
|
|
|
wxLogTrace( tracePdfPlotter, "Stroke path word='%s' wantItalic=%d aItalic=%d aBold=%d",
|
|
TO_UTF8( aWord ), (int) ( aItalic || ( aTextStyle & TEXT_STYLE::ITALIC ) ), (int) aItalic, (int) aBold );
|
|
|
|
std::vector<PDF_STROKE_FONT_RUN> runs;
|
|
m_strokeFontManager->EncodeString( aWord, &runs, aBold, aItalic );
|
|
|
|
if( !runs.empty() )
|
|
{
|
|
VECTOR2D dev_size = userToDeviceSize( aSize );
|
|
double fontSize = dev_size.y;
|
|
|
|
double adj_c = ctm_c;
|
|
double adj_d = ctm_d;
|
|
|
|
if( aItalic && ( !aFont || !aFont->IsOutline() ) )
|
|
{
|
|
double tilt = -ITALIC_TILT;
|
|
if( wideningFactor < 0 )
|
|
tilt = -tilt;
|
|
|
|
adj_c -= ctm_a * tilt;
|
|
adj_d -= ctm_b * tilt;
|
|
}
|
|
|
|
fmt::print( m_workFile, "q {:f} {:f} {:f} {:f} {:f} {:f} cm BT {} Tr {:g} Tz ",
|
|
ctm_a, ctm_b, adj_c, adj_d, ctm_e, ctm_f,
|
|
0, // render_mode
|
|
wideningFactor * 100 );
|
|
|
|
for( const PDF_STROKE_FONT_RUN& run : runs )
|
|
{
|
|
fmt::print( m_workFile, "{} {:g} Tf {} Tj ",
|
|
run.m_subset->ResourceName(),
|
|
fontSize,
|
|
encodeByteString( run.m_bytes ) );
|
|
}
|
|
|
|
fmt::println( m_workFile, "ET" );
|
|
fmt::println( m_workFile, "Q" );
|
|
}
|
|
}
|
|
|
|
return nextPos;
|
|
}
|
|
|
|
|
|
VECTOR2I PDF_PLOTTER::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<OverbarInfo>& aOverbars )
|
|
{
|
|
VECTOR2I nextPosition = aPosition;
|
|
|
|
if( !aNode )
|
|
return nextPosition;
|
|
|
|
TEXT_STYLE_FLAGS currentStyle = aTextStyle;
|
|
VECTOR2I currentSize = aBaseSize;
|
|
bool drawOverbar = false;
|
|
|
|
// Handle markup node types
|
|
if( !aNode->is_root() )
|
|
{
|
|
if( aNode->isSubscript() )
|
|
{
|
|
currentStyle |= TEXT_STYLE::SUBSCRIPT;
|
|
// Subscript: smaller size and lower position
|
|
currentSize = VECTOR2I( aBaseSize.x * 0.5, aBaseSize.y * 0.6 );
|
|
}
|
|
else if( aNode->isSuperscript() )
|
|
{
|
|
currentStyle |= TEXT_STYLE::SUPERSCRIPT;
|
|
// Superscript: smaller size and higher position
|
|
currentSize = VECTOR2I( aBaseSize.x * 0.5, aBaseSize.y * 0.6 );
|
|
}
|
|
|
|
if( aNode->isOverbar() )
|
|
{
|
|
drawOverbar = true;
|
|
// Overbar doesn't change font size, just adds decoration
|
|
}
|
|
|
|
// Render content of this node if it has text
|
|
if( aNode->has_content() )
|
|
{
|
|
wxString nodeText = aNode->asWxString();
|
|
|
|
// Process text content (simplified version of the main text processing)
|
|
wxStringTokenizer str_tok( nodeText, " ", wxTOKEN_RET_DELIMS );
|
|
|
|
while( str_tok.HasMoreTokens() )
|
|
{
|
|
wxString word = str_tok.GetNextToken();
|
|
nextPosition = renderWord( word, nextPosition, currentSize, aOrient, aTextMirrored,
|
|
aWidth, aBaseBold || (currentStyle & TEXT_STYLE::BOLD),
|
|
aBaseItalic || (currentStyle & TEXT_STYLE::ITALIC),
|
|
aFont, aFontMetrics, aV_justify, currentStyle );
|
|
}
|
|
}
|
|
}
|
|
|
|
// Process child nodes recursively
|
|
for( const std::unique_ptr<MARKUP::NODE>& child : aNode->children )
|
|
{
|
|
VECTOR2I startPos = nextPosition;
|
|
|
|
nextPosition = renderMarkupNode( child.get(), nextPosition, currentSize, aOrient,
|
|
aTextMirrored, aWidth, aBaseBold, aBaseItalic,
|
|
aFont, aFontMetrics, aV_justify, currentStyle, aOverbars );
|
|
|
|
// Store overbar info for later rendering
|
|
if( drawOverbar )
|
|
{
|
|
VECTOR2I endPos = nextPosition;
|
|
aOverbars.push_back( { startPos, endPos, currentSize, aFont && aFont->IsOutline(), aV_justify } );
|
|
}
|
|
}
|
|
|
|
return nextPosition;
|
|
}
|
|
|
|
|
|
void PDF_PLOTTER::drawOverbars( const std::vector<OverbarInfo>& aOverbars,
|
|
const EDA_ANGLE& aOrient, const KIFONT::METRICS& aFontMetrics )
|
|
{
|
|
for( const OverbarInfo& overbar : aOverbars )
|
|
{
|
|
// Baseline direction (vector from start to end). If zero length, derive from orientation.
|
|
VECTOR2D dir( overbar.endPos.x - overbar.startPos.x,
|
|
overbar.endPos.y - overbar.startPos.y );
|
|
|
|
double len = hypot( dir.x, dir.y );
|
|
if( len <= 1e-6 )
|
|
{
|
|
// Fallback: derive direction from orientation angle
|
|
double ang = aOrient.AsRadians();
|
|
dir.x = cos( ang );
|
|
dir.y = sin( ang );
|
|
len = 1.0;
|
|
}
|
|
|
|
dir.x /= len;
|
|
dir.y /= len;
|
|
|
|
// Perpendicular (rotate dir 90° CCW). Upward in text space so overbar sits above baseline.
|
|
VECTOR2D nrm( -dir.y, dir.x );
|
|
|
|
// Base vertical offset distance in device units (baseline -> default overbar position)
|
|
double barOffset = aFontMetrics.GetOverbarVerticalPosition( overbar.fontSize.y );
|
|
|
|
// Adjust further for outline fonts
|
|
if( overbar.isOutline )
|
|
barOffset += overbar.fontSize.y * 0.25; // extra raise for outline font
|
|
|
|
// Mirror the text vertical alignment adjustments used for baseline shifting.
|
|
// Earlier logic scales baseline adjustment: CENTER ~2x, TOP ~4x. We apply proportional
|
|
// extra raise so that overbars track visually with perceived baseline shift.
|
|
double alignMult = 1.0;
|
|
|
|
switch( overbar.vAlign )
|
|
{
|
|
case GR_TEXT_V_ALIGN_CENTER: alignMult = overbar.isOutline ? 2.0 : 1.0; break;
|
|
case GR_TEXT_V_ALIGN_TOP: alignMult = overbar.isOutline ? 4.0 : 1.0; break;
|
|
default: alignMult = 1.0; break; // bottom
|
|
}
|
|
|
|
if( alignMult > 1.0 )
|
|
{
|
|
// Scale only the baseline component (approx 17% of height, matching earlier baseline_factor)
|
|
double baseline_factor = 0.17;
|
|
barOffset += ( alignMult - 1.0 ) * ( baseline_factor * overbar.fontSize.y );
|
|
}
|
|
|
|
// Trim to avoid rounded cap extension (assumes stroke caps); proportion of font width.
|
|
double barTrim = overbar.fontSize.x * 0.1;
|
|
|
|
// Apply trim along baseline direction and offset along normal
|
|
VECTOR2D startPt( overbar.startPos.x, overbar.startPos.y );
|
|
VECTOR2D endPt( overbar.endPos.x, overbar.endPos.y );
|
|
|
|
// Both endpoints should share identical vertical (normal) offset above baseline.
|
|
// Use a single offset vector offVec = -barOffset * nrm (negative because nrm points 'up').
|
|
VECTOR2D offVec( -barOffset * nrm.x, -barOffset * nrm.y );
|
|
|
|
startPt.x += dir.x * barTrim + offVec.x;
|
|
startPt.y += dir.y * barTrim + offVec.y;
|
|
endPt.x -= dir.x * barTrim - offVec.x; // subtract trim, then apply same vertical offset
|
|
endPt.y -= dir.y * barTrim - offVec.y;
|
|
|
|
VECTOR2I iStart( KiROUND( startPt.x ), KiROUND( startPt.y ) );
|
|
VECTOR2I iEnd( KiROUND( endPt.x ), KiROUND( endPt.y ) );
|
|
|
|
MoveTo( iStart );
|
|
LineTo( iEnd );
|
|
PenFinish();
|
|
}
|
|
}
|
|
|
|
|
|
void PDF_PLOTTER::PlotText( const VECTOR2I& aPos,
|
|
const COLOR4D& aColor,
|
|
const wxString& aText,
|
|
const TEXT_ATTRIBUTES& aAttributes,
|
|
KIFONT::FONT* aFont,
|
|
const KIFONT::METRICS& aFontMetrics,
|
|
void* aData )
|
|
{
|
|
VECTOR2I size = aAttributes.m_Size;
|
|
|
|
// PDF files do not like 0 sized texts which create broken files.
|
|
if( size.x == 0 || size.y == 0 )
|
|
return;
|
|
|
|
if( aAttributes.m_Mirrored )
|
|
size.x = -size.x;
|
|
|
|
PDF_PLOTTER::Text( aPos, aColor, aText, aAttributes.m_Angle, size, aAttributes.m_Halign,
|
|
aAttributes.m_Valign, aAttributes.m_StrokeWidth, aAttributes.m_Italic,
|
|
aAttributes.m_Bold, aAttributes.m_Multiline, aFont, aFontMetrics, aData );
|
|
}
|
|
|
|
|
|
void PDF_PLOTTER::HyperlinkBox( const BOX2I& aBox, const wxString& aDestinationURL )
|
|
{
|
|
m_hyperlinksInPage.push_back( std::make_pair( aBox, aDestinationURL ) );
|
|
}
|
|
|
|
|
|
void PDF_PLOTTER::HyperlinkMenu( const BOX2I& aBox, const std::vector<wxString>& aDestURLs )
|
|
{
|
|
m_hyperlinkMenusInPage.push_back( std::make_pair( aBox, aDestURLs ) );
|
|
}
|
|
|
|
|
|
void PDF_PLOTTER::Bookmark( const BOX2I& aLocation, const wxString& aSymbolReference,
|
|
const wxString &aGroupName )
|
|
{
|
|
|
|
m_bookmarksInPage[aGroupName].push_back( std::make_pair( aLocation, aSymbolReference ) );
|
|
}
|
|
|
|
|
|
void PDF_PLOTTER::Plot3DModel( const wxString& aSourcePath,
|
|
const std::vector<PDF_3D_VIEW>& a3DViews )
|
|
{
|
|
std::map<float, int> m_fovMap;
|
|
std::vector<int> m_viewHandles;
|
|
|
|
for( const PDF_3D_VIEW& view : a3DViews )
|
|
{
|
|
int fovHandle = -1;
|
|
if( !m_fovMap.contains( view.m_fov ) )
|
|
{
|
|
fovHandle = allocPdfObject();
|
|
m_fovMap[view.m_fov] = fovHandle;
|
|
|
|
startPdfObject( fovHandle );
|
|
fmt::print( m_outputFile,
|
|
"<<\n"
|
|
"/FOV {}\n"
|
|
"/PS /Min\n"
|
|
"/Subtype /P\n"
|
|
">>\n",
|
|
view.m_fov );
|
|
closePdfObject();
|
|
}
|
|
else
|
|
{
|
|
fovHandle = m_fovMap[view.m_fov];
|
|
}
|
|
|
|
int viewHandle = allocPdfObject();
|
|
startPdfObject( viewHandle );
|
|
|
|
fmt::print( m_outputFile,
|
|
"<<\n"
|
|
"/Type /3DView\n"
|
|
"/XN ({})\n"
|
|
"/IN ({})\n"
|
|
"/MS /M\n"
|
|
"/C2W [{:f} {:f} {:f} {:f} {:f} {:f} {:f} {:f} {:f} {:f} {:f} {:f}]\n"
|
|
"/CO {:f}\n"
|
|
"/NR false\n"
|
|
"/BG<<\n"
|
|
"/Type /3DBG\n"
|
|
"/Subtype /SC\n"
|
|
"/CS /DeviceRGB\n"
|
|
"/C [1.000000 1.000000 1.000000]>>\n"
|
|
"/P {} 0 R\n"
|
|
"/LS<<\n"
|
|
"/Type /3DLightingScheme\n"
|
|
"/Subtype /CAD>>\n"
|
|
">>\n",
|
|
view.m_name, view.m_name, view.m_cameraMatrix[0],
|
|
view.m_cameraMatrix[1],
|
|
view.m_cameraMatrix[2], view.m_cameraMatrix[3], view.m_cameraMatrix[4],
|
|
view.m_cameraMatrix[5], view.m_cameraMatrix[6], view.m_cameraMatrix[7],
|
|
view.m_cameraMatrix[8], view.m_cameraMatrix[9], view.m_cameraMatrix[10],
|
|
view.m_cameraMatrix[11],
|
|
view.m_cameraCenter,
|
|
fovHandle );
|
|
|
|
closePdfObject();
|
|
|
|
m_viewHandles.push_back( viewHandle );
|
|
}
|
|
|
|
m_3dModelHandle = startPdfObject();
|
|
|
|
// so we can get remotely stuff the length afterwards
|
|
int modelLenHandle = allocPdfObject();
|
|
|
|
fmt::print( m_outputFile,
|
|
"<<\n"
|
|
"/Type /3D\n"
|
|
"/Subtype /U3D\n"
|
|
"/DV 0\n" );
|
|
|
|
fmt::print( m_outputFile, "/VA [" );
|
|
for( int viewHandle : m_viewHandles )
|
|
{
|
|
fmt::print( m_outputFile, "{} 0 R ", viewHandle );
|
|
}
|
|
fmt::print( m_outputFile, "]\n" );
|
|
|
|
fmt::print( m_outputFile,
|
|
"/Length {} 0 R\n"
|
|
"/Filter /FlateDecode\n"
|
|
">>\n", // Length is deferred
|
|
modelLenHandle );
|
|
|
|
fmt::println( m_outputFile, "stream" );
|
|
|
|
wxFFile outputFFile( m_outputFile );
|
|
|
|
fflush( m_outputFile );
|
|
long imgStreamStart = ftell( m_outputFile );
|
|
|
|
size_t model_stored_size = 0;
|
|
{
|
|
wxFFileOutputStream ffos( outputFFile );
|
|
wxZlibOutputStream zos( ffos, wxZ_BEST_COMPRESSION, wxZLIB_ZLIB );
|
|
|
|
wxFFileInputStream fileStream( aSourcePath );
|
|
if( !fileStream.IsOk() )
|
|
{
|
|
wxLogError( _( "Failed to open 3D model file: %s" ), aSourcePath );
|
|
}
|
|
|
|
zos.Write( fileStream );
|
|
}
|
|
fflush( m_outputFile );
|
|
model_stored_size = ftell( m_outputFile );
|
|
model_stored_size -= imgStreamStart; // Get the size of the compressed stream
|
|
|
|
fmt::print( m_outputFile, "\nendstream\n" );
|
|
closePdfObject();
|
|
|
|
startPdfObject( modelLenHandle );
|
|
fmt::println( m_outputFile, "{}", (unsigned) model_stored_size );
|
|
closePdfObject();
|
|
|
|
outputFFile.Detach(); // Don't close it
|
|
}
|