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= a directory with additional fonts font= an additional font filename pages= the range of pages to convert (example: pages=1-100,210-) zoom= the resultion (default: 72) languagedir= Add an xpdf language directory multiply= Render everything at the resolution poly2bitmap Convert graphics to bitmaps bitmap Convert everything to bitmaps SWF Parameters: SWF layer options: jpegsubpixels= resolution adjustment for jpeg images (same as jpegdpi, but in pixels) ppmsubpixels= 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==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= when the user clicks a internal link (to a different page) in the converted file, this actionscript function is called externallinkfunction= 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= 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= the SWF fileversion (6) framerate= SWF framerate minlinewidth= 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= set compression quality of jpeg images splinequality= 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)