gp_render/OP_render_pdf.py

422 lines
18 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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)