422 lines
18 KiB
Python
422 lines
18 KiB
Python
import bpy
|
||
import sys
|
||
import subprocess
|
||
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)]
|
||
|
||
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
|
||
|
||
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')
|
||
|
||
|
||
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
|
||
'''
|
||
scn = bpy.context.scene
|
||
|
||
if multi_page:
|
||
## Set folder name (when using sequence) as output with pdf extension
|
||
fp = fp.parent
|
||
|
||
fp.parent.mkdir(parents=True, exist_ok=True) # mode=0o777
|
||
|
||
## Single pdf
|
||
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
|
||
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')
|
||
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),
|
||
# '-o', # seems like there is no need to add output flag
|
||
str(swf_fp)
|
||
]
|
||
subprocess.call(cmd)
|
||
|
||
# Remove single pdf file
|
||
pdf_fp.unlink()
|
||
return swf_fp
|
||
|
||
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
|
||
|
||
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'
|
||
|
||
def execute(self, context):
|
||
### store
|
||
## dict all visible objects as key with value : sub dict {layer : hide_bool}
|
||
|
||
# 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 = []
|
||
|
||
## 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
|
||
|
||
mess = f'--- {ob.name}:'
|
||
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
|
||
|
||
## 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:
|
||
mess = f'/!\ {vl_name} viewlayer not exists : skipped {[l.info for l in layer_list]}'
|
||
print(mess)
|
||
messages.append(mess)
|
||
continue
|
||
if vl_name in {'exclude', 'View Layer'}:
|
||
continue
|
||
|
||
if not vl.use:
|
||
mess = f'{vl_name} viewlayer disabled'
|
||
print(mess)
|
||
messages.append(mess)
|
||
continue
|
||
|
||
|
||
# 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:
|
||
mess = f'Skip {vl_name}: Not found nodegroup {ng_name}'
|
||
print(mess)
|
||
messages.append(mess)
|
||
continue
|
||
|
||
ng_socket = ng.outputs.get(vl_name)
|
||
|
||
if not ng_socket:
|
||
mess = f'Skip {vl_name}: Not found in nodegroup {ng_name} sockets'
|
||
print(mess)
|
||
messages.append(mess)
|
||
continue
|
||
|
||
if not len(ng_socket.links):
|
||
mess = f' socket is disconnected in {ng_name} nodegroup'
|
||
print(mess)
|
||
messages.append(mess)
|
||
continue
|
||
|
||
fo_node = ng_socket.links[0].to_node
|
||
fo_socket = ng_socket.links[0].to_socket
|
||
|
||
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
|
||
|
||
if fo_node.mute:
|
||
mess = f'Skip {vl_name}: output is muted {fo_node.name}'
|
||
print(mess)
|
||
messages.append(mess)
|
||
continue
|
||
|
||
# fo_socket.name isn't right
|
||
|
||
idx = [i for i in fo_node.inputs].index(fo_socket)
|
||
|
||
subpath = fo_node.file_slots[idx].path
|
||
fp = Path(fo_node.base_path.rstrip('/')) / subpath
|
||
fp = Path(bpy.path.abspath(str(fp)).rstrip("/"))
|
||
|
||
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:
|
||
l.hide = l not in layer_list or store[ob][l]
|
||
|
||
for l in gpl:
|
||
if not l.hide:
|
||
print(f'-> {l.info}') #Dbg
|
||
|
||
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 ?
|
||
pdf_render(fp, multi_page=True)
|
||
elif self.export_type == 'swf':
|
||
swf_render(fp)
|
||
|
||
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
|
||
|
||
for o in selection:
|
||
o.select_set(True)
|
||
if act:
|
||
context.view_layer.objects.active = act
|
||
|
||
# for oviz in obj_vis:
|
||
# oviz[0].hide_viewport = oviz[1]
|
||
|
||
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)')
|
||
return {"FINISHED"}
|
||
|
||
|
||
classes=(
|
||
GPEXP_OT_export_as_pdf,
|
||
)
|
||
|
||
def register():
|
||
for cls in classes:
|
||
bpy.utils.register_class(cls)
|
||
|
||
def unregister():
|
||
for cls in reversed(classes):
|
||
bpy.utils.unregister_class(cls)
|