2021-09-24 18:36:58 +02:00
import bpy
2023-06-29 18:15:59 +02:00
import sys
import subprocess
2021-09-24 18:36:58 +02:00
from . import fn
from pathlib import Path
from itertools import groupby
from pprint import pprint as pp
from time import time , strftime
def export_all_selected_frame_as_svg ( ) :
''' Export All frames (only where there is a frame) of selected layer as svg '''
### Export operator parameters description
# use_fill (boolean, (optional)) – Fill, Export strokes with fill enabled
# selected_object_type (enum in ['ACTIVE', 'SELECTED', 'VISIBLE'], (optional)) –
# Object, Which objects to include in the export
# ACTIVE Active, Include only the active object.
# SELECTED Selected, Include selected objects.
# VISIBLE Visible, Include all visible objects.
#
# stroke_sample (float in [0, 100], (optional)) – Sampling, Precision of stroke sampling. Low values mean a more precise result, and zero disables sampling
# use_normalized_thickness (boolean, (optional)) – Normalize, Export strokes with constant thickness
# use_clip_camera (boolean, (optional)) – Clip Camera, Clip drawings to camera size when export in camera view
## Write an ouput name (folder and image will use this name)
## if left empty, will use name of active object with 'svg_' prefix
name = ' '
only_frames = 1 # put 0 to export whole frame range
## ----
o = bpy . context . object
assert o . type == ' GPENCIL ' , ' Active object should be GP '
if only_frames :
frames = [ ]
for ob in bpy . context . selected_objects :
if ob . type != ' GPENCIL ' :
continue
frames + = [ f . frame_number for l in ob . data . layers if not l . hide for f in l . frames if len ( f . strokes ) ]
2023-01-18 14:28:27 +01:00
2021-09-24 18:36:58 +02:00
if frames :
frames = sorted ( list ( set ( frames ) ) )
else :
frames = [ f for f in range ( bpy . context . scene . frame_start , bpy . context . scene . frame_end + 1 ) ]
print ( len ( frames ) , ' frames to export ' )
pass_name = name if name else f ' svg_ { o . name } '
blend = Path ( bpy . data . filepath )
for fnum in frames :
out = f ' { pass_name } _ { fnum : 04d } .svg '
print ( out )
folder = blend . parent / ' render ' / pass_name
folder . mkdir ( parents = True , exist_ok = True )
fp = folder / out
if fp . exists ( ) :
print ( f ' already exists: { fp } ' )
continue
2023-01-18 14:28:27 +01:00
2021-09-24 18:36:58 +02:00
bpy . context . scene . frame_current = fnum
bpy . ops . wm . gpencil_export_svg ( filepath = str ( fp ) ,
check_existing = True ,
use_fill = True , selected_object_type = ' SELECTED ' , # ACTIVE, VISIBLE
stroke_sample = 0.0 ,
use_normalized_thickness = False ,
use_clip_camera = True ) # False by defaut
print ( ' Done ' )
2023-06-29 18:15:59 +02:00
def pdf_render ( fp , multi_page = False ) :
''' Render a sequence of pdf and return path to folder
or render a multipage pdf ( in place of parent folder ) and return path to file
'''
2021-09-24 18:36:58 +02:00
scn = bpy . context . scene
2023-06-29 18:15:59 +02:00
if multi_page :
## Set folder name (when using sequence) as output with pdf extension
fp = fp . parent
2021-09-24 18:36:58 +02:00
fp . parent . mkdir ( parents = True , exist_ok = True ) # mode=0o777
2023-06-29 18:15:59 +02:00
2023-07-13 18:50:00 +02:00
## Single pdf
2023-06-29 18:15:59 +02:00
if multi_page :
## In case of sequence path:
## Remove padding separator (or add scene range: "0100-0230" ?)
# fp = fp.with_stem(fp.stem.rstrip('-_. ')) # strip typical padding separator
fp = fp . with_suffix ( ' .pdf ' )
# print('Exporting multi-page pdf at: ', fp)
bpy . ops . wm . gpencil_export_pdf ( filepath = str ( fp ) ,
check_existing = False , # True by default
use_fill = True ,
selected_object_type = ' ACTIVE ' , # VISIBLE, SELECTED
stroke_sample = 0 ,
use_normalized_thickness = False ,
frame_mode = ' SCENE ' )
return fp
## Sequence
2021-09-24 18:36:58 +02:00
for fnum in range ( scn . frame_start , scn . frame_end + 1 ) :
# print('fnum: ', fnum)
scn . frame_current = fnum
# bpy.ops.wm.gpencil_export_svg(filepath=str(fp),
# check_existing=True,
# use_fill=True, selected_object_type='SELECTED', # ACTIVE, VISIBLE
# stroke_sample=0.0,
# use_normalized_thickness=False,
# use_clip_camera=True) # False by defaut
# bpy.ops.wm.gpencil_export_pdf(filepath=f'{bpy.path.abspath(str(fp)).rstrip("/")}{fnum:04d}.pdf',
bpy . ops . wm . gpencil_export_pdf ( filepath = f ' { fp } { fnum : 04d } .pdf ' ,
check_existing = False , # True by default
use_fill = True ,
selected_object_type = ' ACTIVE ' , # VISIBLE, SELECTED
stroke_sample = 0 ,
use_normalized_thickness = False ,
frame_mode = ' ACTIVE ' )
2023-06-29 18:15:59 +02:00
return fp . parent
def swf_render ( fp ) :
'''
Render as pdf then convert to swf using pdf2swf
- h , - - help Print short help message and exit
- V , - - version Print version info and exit
- o , - - output file . swf Direct output to file . swf . If file . swf contains ' % ' ( file % . swf ) , then each page goes to a separate file .
- p , - - pages range Convert only pages in range with range e . g . 1 - 20 or 1 , 4 , 6 , 9 - 11 or
- P , - - password password Use password for deciphering the pdf .
- v , - - verbose Be verbose . Use more than one - v for greater effect .
- z , - - zlib Use Flash 6 ( MX ) zlib compression .
- i , - - ignore Allows pdf2swf to change the draw order of the pdf . This may make the generated
- j , - - jpegquality quality Set quality of embedded jpeg pictures to quality . 0 is worst ( small ) , 100 is best ( big ) . ( default : 85 )
- s , - - set param = value Set a SWF encoder specific parameter . See pdf2swf - s help for more information .
- w , - - samewindow When converting pdf hyperlinks , don ' t make the links open a new window.
- t , - - stop Insert a stop ( ) command in each page .
- T , - - flashversion num Set Flash Version in the SWF header to num .
- F , - - fontdir directory Add directory to the font search path .
- b , - - defaultviewer Link a standard viewer to the swf file .
- l , - - defaultloader Link a standard preloader to the swf file which will be displayed while the main swf is loading .
- B , - - viewer filename Link viewer filename to the swf file .
- L , - - preloader filename Link preloader filename to the swf file .
- q , - - quiet Suppress normal messages . Use - qq to suppress warnings , also .
- S , - - shapes Don ' t use SWF Fonts, but store everything as shape.
- f , - - fonts Store full fonts in SWF . ( Don ' t reduce to used characters).
- G , - - flatten Remove as many clip layers from file as possible .
- I , - - info Don ' t do actual conversion, just display a list of all pages in the PDF.
- Q , - - maxtime n Abort conversion after n seconds . Only available on Unix .
##with -s arg:
PDF Parameters :
PDF device global parameters :
fontdir = < dir > a directory with additional fonts
font = < filename > an additional font filename
pages = < range > the range of pages to convert ( example : pages = 1 - 100 , 210 - )
zoom = < dpi > the resultion ( default : 72 )
languagedir = < dir > Add an xpdf language directory
multiply = < times > Render everything at < times > the resolution
poly2bitmap Convert graphics to bitmaps
bitmap Convert everything to bitmaps
SWF Parameters :
SWF layer options :
jpegsubpixels = < pixels > resolution adjustment for jpeg images ( same as jpegdpi , but in pixels )
ppmsubpixels = < pixels resolution adjustment for lossless images ( same as ppmdpi , but in pixels )
subpixels = < pixels > shortcut for setting both jpegsubpixels and ppmsubpixels
drawonlyshapes convert everything to shapes ( currently broken )
ignoredraworder allow to perform a few optimizations for creating smaller SWFs
linksopennewwindow make links open a new browser window
linktarget target window name of new links
linkcolor = < color ) color of links ( format : RRGGBBAA )
linknameurl Link buttons will be named like the URL they refer to ( handy for iterating through links with actionscript )
storeallcharacters don ' t reduce the fonts to used characters in the output file
enablezlib switch on zlib compression ( also done if flashversion > = 6 )
bboxvars store the bounding box of the SWF file in actionscript variables
dots Take care to handle dots correctly
reordertags = 0 / 1 ( default : 1 ) perform some tag optimizations
internallinkfunction = < name > when the user clicks a internal link ( to a different page ) in the converted file , this actionscript function is called
externallinkfunction = < name > when the user clicks an external link ( e . g . http : / / www . foo . bar / ) on the converted file , this actionscript function is called
disable_polygon_conversion never convert strokes to polygons ( will remove capstyles and joint styles )
caplinewidth = < width > the minimum thichness a line needs to have so that capstyles become visible ( and are converted )
insertstop put an ActionScript " STOP " tag in every frame
protect add a " protect " tag to the file , to prevent loading in the Flash editor
flashversion = < version > the SWF fileversion ( 6 )
framerate = < fps > SWF framerate
minlinewidth = < width > convert horizontal / vertical boxes smaller than this width to lines ( 0.05 )
simpleviewer Add next / previous buttons to the SWF
animate insert a showframe tag after each placeobject ( animate draw order of PDF files )
jpegquality = < quality > set compression quality of jpeg images
splinequality = < value > Set the quality of spline convertion to value ( 0 - 100 , default : 100 ) .
disablelinks Disable links .
'''
# Convert pdf to swf
if sys . platform == ' linux ' :
binary = Path ( __file__ ) . parent / ' bin ' / ' linux ' / ' pdf2swf '
elif sys . platform . startswith ( ' win ' ) :
binary = Path ( __file__ ) . parent / ' bin ' / ' windows ' / ' pdf2swf.exe '
else :
print ( ' Render to SWF is only supported on mat and linux ' )
return
pdf_fp = pdf_render ( fp , multi_page = True )
swf_fp = pdf_fp . with_suffix ( ' .swf ' )
cmd = [
str ( binary ) ,
' -s ' , f ' framerate= { bpy . context . scene . render . fps } ' ,
# -z, # use zlib
str ( pdf_fp ) ,
2023-07-13 18:50:00 +02:00
# '-o', # seems like there is no need to add output flag
2023-06-29 18:15:59 +02:00
str ( swf_fp )
]
subprocess . call ( cmd )
# Remove single pdf file
pdf_fp . unlink ( )
return swf_fp
2021-09-24 18:36:58 +02:00
class GPEXP_OT_export_as_pdf ( bpy . types . Operator ) :
bl_idname = " gp.export_as_pdf "
bl_label = " export_as_pdf "
bl_description = " Export current layers as pdf "
bl_options = { " REGISTER " }
@classmethod
def poll ( cls , context ) :
return True
2023-06-29 18:15:59 +02:00
export_type : bpy . props . StringProperty ( name = ' Export Type ' , default = ' pdf_sequence ' )
@classmethod
def description ( cls , context , properties ) - > str :
if properties . export_type == ' pdf ' :
return ' Export layers as individual pdf file (multi-page) using scene range '
elif properties . export_type == ' pdf_sequence ' :
return ' Export layers as sequence of single-paged pdf file, using scene range '
return ' Export layers as individual SWF files, using scene range '
2023-01-18 14:28:27 +01:00
def execute ( self , context ) :
2021-09-24 18:36:58 +02:00
### store
## dict all visible objects as key with value : sub dict {layer : hide_bool}
2023-01-18 14:28:27 +01:00
2021-09-24 18:36:58 +02:00
# obj_vis = [[o, o.hide_viewport, o.hide_render] for o in context.scene.objects if o.type == 'GPENCIL' and not (o.hide_get() or o.hide_viewport)]
t0 = time ( )
store = { o : { l : l . hide for l in o . data . layers } for o in context . scene . objects if o . type == ' GPENCIL ' and not ( o . hide_get ( ) or o . hide_viewport ) }
# pp(store)
act = context . object if context . object else None
selection = [ o for o in context . selected_objects ]
messages = [ ]
2023-01-18 14:28:27 +01:00
2021-09-24 18:36:58 +02:00
## adaptative resampling on all concerned objects
for ob in store . keys ( ) :
mod = ob . grease_pencil_modifiers . get ( ' resample ' )
if not mod :
mod = ob . grease_pencil_modifiers . new ( ' resample ' , ' GP_SIMPLIFY ' )
mod . mode = ' ADAPTIVE '
mod . factor = 0.001
# for ob in context.scene.objects:
for ob in store . keys ( ) :
if ob . type != ' GPENCIL ' :
continue
2023-01-18 14:28:27 +01:00
mess = f ' --- { ob . name } : '
2021-09-24 18:36:58 +02:00
print ( mess )
messages . append ( mess )
## swap hide other GP object (or just swap select)
# for so in store.keys():
# so.hide_viewport = True
# ob.hide_viewport = False
context . view_layer . objects . active = ob # render active only mode
# for o in context.scene.objects:
# o.hide_viewport = True
# ob.hide_viewport = False
2023-01-18 14:28:27 +01:00
2021-09-24 18:36:58 +02:00
## manage layers
gpl = ob . data . layers
vl_dicts = { vl_name : list ( layer_grp ) for vl_name , layer_grp in groupby ( gpl , lambda x : x . viewlayer_render ) }
for vl_name , layer_list in vl_dicts . items ( ) :
vl = context . scene . view_layers . get ( vl_name )
if not vl :
2023-01-18 14:28:27 +01:00
mess = f ' /! \ { vl_name } viewlayer not exists : skipped { [ l . info for l in layer_list ] } '
2021-09-24 18:36:58 +02:00
print ( mess )
messages . append ( mess )
continue
if vl_name in { ' exclude ' , ' View Layer ' } :
continue
if not vl . use :
2023-01-18 14:28:27 +01:00
mess = f ' { vl_name } viewlayer disabled '
2021-09-24 18:36:58 +02:00
print ( mess )
messages . append ( mess )
continue
2023-01-18 14:28:27 +01:00
2021-09-24 18:36:58 +02:00
# Case of initially masked layer !
hide_ct = 0
total = len ( layer_list )
for l in layer_list :
if store [ ob ] [ l ] : # get original hide bool
hide_ct + = 1
if hide_ct == total :
mess = f ' /! \ Skip { vl_name } : { hide_ct } / { total } are hided '
print ( mess )
messages . append ( mess )
continue
elif hide_ct > 0 :
mess = f ' Warning: { vl_name } : { hide_ct } / { total } are hided '
print ( mess )
messages . append ( mess )
# check connections in compositor
ng_name = f ' NG_ { ob . name } '
ng = context . scene . node_tree . nodes . get ( ng_name )
if not ng :
2023-01-18 14:28:27 +01:00
mess = f ' Skip { vl_name } : Not found nodegroup { ng_name } '
2021-09-24 18:36:58 +02:00
print ( mess )
messages . append ( mess )
continue
2023-01-18 14:28:27 +01:00
2021-09-24 18:36:58 +02:00
ng_socket = ng . outputs . get ( vl_name )
2023-01-18 14:28:27 +01:00
2021-09-24 18:36:58 +02:00
if not ng_socket :
2023-01-18 14:28:27 +01:00
mess = f ' Skip { vl_name } : Not found in nodegroup { ng_name } sockets '
2021-09-24 18:36:58 +02:00
print ( mess )
messages . append ( mess )
continue
2023-01-18 14:28:27 +01:00
2021-09-24 18:36:58 +02:00
if not len ( ng_socket . links ) :
2023-01-18 14:28:27 +01:00
mess = f ' socket is disconnected in { ng_name } nodegroup '
2021-09-24 18:36:58 +02:00
print ( mess )
messages . append ( mess )
continue
fo_node = ng_socket . links [ 0 ] . to_node
fo_socket = ng_socket . links [ 0 ] . to_socket
2023-01-18 14:28:27 +01:00
2021-09-24 18:36:58 +02:00
if fo_node . type != ' OUTPUT_FILE ' :
mess = f ' Skip { vl_name } : node is not an output_file { fo_node . name } '
print ( mess )
messages . append ( mess )
continue
2023-01-18 14:28:27 +01:00
2021-09-28 11:01:49 +02:00
if fo_node . mute :
mess = f ' Skip { vl_name } : output is muted { fo_node . name } '
print ( mess )
messages . append ( mess )
continue
2021-09-24 18:36:58 +02:00
# fo_socket.name isn't right
idx = [ i for i in fo_node . inputs ] . index ( fo_socket )
2023-06-29 18:15:59 +02:00
2021-09-24 18:36:58 +02:00
subpath = fo_node . file_slots [ idx ] . path
fp = Path ( fo_node . base_path . rstrip ( ' / ' ) ) / subpath
fp = Path ( bpy . path . abspath ( str ( fp ) ) . rstrip ( " / " ) )
2023-01-18 14:28:27 +01:00
2021-09-24 18:36:58 +02:00
print ( f ' render { total } layers at: { fp . parent } ' ) #Dbg
# hide all layer that are: not associated with VL (not in layer_list) or hided initially (store[ob][l])
for l in gpl :
2023-01-18 14:28:27 +01:00
l . hide = l not in layer_list or store [ ob ] [ l ]
2021-09-24 18:36:58 +02:00
for l in gpl :
if not l . hide :
print ( f ' -> { l . info } ' ) #Dbg
2023-06-29 18:15:59 +02:00
if self . export_type == ' pdf_sequence ' :
pdf_render ( fp , multi_page = False )
elif self . export_type == ' pdf ' :
## TODO: change fp to parent and adapt in pdf render ?
2023-07-13 18:50:00 +02:00
pdf_render ( fp , multi_page = True )
2023-06-29 18:15:59 +02:00
elif self . export_type == ' swf ' :
swf_render ( fp )
2021-09-24 18:36:58 +02:00
print ( )
### restore
for ob , layer_dic in store . items ( ) :
# ob.hide_viewport = False # no need
for l , h in layer_dic . items ( ) :
l . hide = h
2023-01-18 14:28:27 +01:00
2021-09-24 18:36:58 +02:00
for o in selection :
o . select_set ( True )
if act :
context . view_layer . objects . active = act
2023-01-18 14:28:27 +01:00
2021-09-24 18:36:58 +02:00
# for oviz in obj_vis:
# oviz[0].hide_viewport = oviz[1]
2021-09-28 11:01:49 +02:00
print ( f ' Done ( { time ( ) - t0 : .1f } s) ' )
# self.report({'INFO'}, f'Done ({time()-t0:.1f}s)')
fn . show_message_box ( _message = messages , _title = f ' PDF render report ( { time ( ) - t0 : .1f } s) ' )
2021-09-24 18:36:58 +02:00
return { " FINISHED " }
classes = (
GPEXP_OT_export_as_pdf ,
)
2023-01-18 14:28:27 +01:00
def register ( ) :
2021-09-24 18:36:58 +02:00
for cls in classes :
bpy . utils . register_class ( cls )
def unregister ( ) :
for cls in reversed ( classes ) :
2023-07-13 18:50:00 +02:00
bpy . utils . unregister_class ( cls )