基于CUPS实现云打印平台
今年在员工服务台上线了云打印功能,不少小伙伴私下联系好奇是如何实现的,连公司打印机供应商都惊动了,于是去翻了下笔记,单开一篇文档简单说明下实现过程吧,
设计思路
CUPS(Common UNIX Printing System,通用 UNIX 打印系统)是一个开源的打印系统,通过实现标准的打印协议(如 IPP)以及提供强大的管理工具,为用户和管理员简化打印任务的配置和管理。本服务主要是依赖CUPS打印服务实现,大致设计思路如下:
用户前端浏览器上传文件,发送文件及打印参数至后端接口
后端接收文件和打印参数,创建打印任务队列调用打印工具类lp
通过lp命令发送文件至cups服务器实现打印
业务流程图和前端页面:
打印服务端实现
安装cups和过滤器依赖
# 我这里使用的是1.6.3版本,openprinting最新版本是2.4,这个版本耦合性太强,安装filters时编译复杂,太多依赖需要手动安装,这里功能不需要太复杂,故而选择旧版本
yum install -y cups foomatic-filters
编辑配置
/etc/cups/cupsd.conf
Listen 0.0.0.0:631
# 增加Allow all
# Restrict access to the server...
<Location />
Allow all
Order allow,deny
</Location>
# Restrict access to the admin pages...
<Location /admin>
Allow all
AuthType Default
Require user @SYSTEM
Order allow,deny
</Location>
# Restrict access to configuration files...
<Location /admin/conf>
Allow all
AuthType Default
Require user @SYSTEM
Order allow,deny
</Location>
重启服务
systemctl restart cups
下载打印机ppd驱动描述文件
[理光打印机] https://www.openprinting.org/download/PPD/Ricoh/PS/
[其他型号] https://www.openprinting.org/download/PPD/
回到CUPS管理后台,添加打印机
打印命令配置测试
使用lp打印命令测试驱动配置,打印选项根据机型和驱动版本有所不同,具体参数可使用lpoptions -d -l printername
查看
理光
# 页面自适应
-o fit-to-page
# 打印份数
-o copies=1
# 颜色模式:Gray黑白,CMYK彩色
-o ColorModel=Gray/CMKY
# 双面模式:None关闭,DuplexNoTumble长边翻转,DuplexTumble短边翻转
-o Duplex=None/DuplexNoTumble/DuplexTumble
# 横向打印,仅文本模式
-o landscape
# 双面模式:one-sided单面,two-sided-long-edge双面长边翻转
-o sides=one-sided/two-sided-long-edge
震旦(仅差异部分)
#颜色模式:Grayscale黑白,Color彩色
-o SelectColor=Grayscale/Color
后端接口实现
class PrintJobSerializer(serializers.Serializer):
"""
打印接口序列化器
*cups_server:cups服务器和端口号,例127.0.0.1:631
*printer_name:cups中打印机的名称
*file:打印文件
copies:打印份数,默认为1
duplex:双面参数,默认为双面长边翻转(one-sided单面;two-sided-long-edge双面长边翻转)
landscape:打印方向,默认为纵向。(True横向;False纵向)
page_range:页码范围,默认打印全部
color_mode:色彩模式,默认黑白(Gray黑白;CMYK彩色)
fit_to_page:页面缩放,默认为填充(True填充;False原始比例)
"""
cups_server = serializers.CharField(required=True)
printer_name = serializers.CharField(required=True)
file = serializers.ListField(child=serializers.FileField(), required=True)
copies = serializers.IntegerField(default=1)
duplex = serializers.CharField(default="two-sided-long-edge")
landscape = serializers.BooleanField(default=False)
page_range = serializers.CharField(default=None)
color_mode = serializers.CharField(default="Gray")
fit_to_page = serializers.BooleanField(default=True)
class PrintJobView(APIView):
authentication_classes = []
def post(self, request, *args, **kwargs):
serializer = PrintJobSerializer(data=request.data)
if serializer.is_valid():
files = serializer.validated_data['file']
files_path = []
for file in files:
random_filename = str(uuid.uuid4()) + '_' + os.path.basename(file.name)
file_path = os.path.join('media', random_filename)
files_path.append(file_path)
with open(file_path, 'wb') as destination:
for chunk in file.chunks():
destination.write(chunk)
try:
print_job_command = [
"lp",
"-h", str(serializer.validated_data['cups_server']),
"-d", serializer.validated_data['printer_name'],
"-n", str(serializer.validated_data['copies']),
"-o", f"sides={serializer.validated_data['duplex']}",
"-o", f"ColorModel={serializer.validated_data['color_mode']}",
]
if serializer.validated_data['landscape']:
print_job_command.extend(["-o", "landscape"])
if serializer.validated_data['fit_to_page']:
print_job_command.extend(["-o", "fit-to-page"])
if serializer.validated_data.get('page_range'):
print_job_command.extend(["-o", f"page-ranges={serializer.validated_data['page_range']}"])
for command_file_path in files_path:
print_job_command.append(command_file_path)
result = subprocess.run(print_job_command, capture_output=True, text=True, shell=True)
# 生产环境需要去掉shell=True,否则无法打印
# result = subprocess.run(print_job_command, capture_output=True, text=True)
if result.returncode != 0:
raise CalledProcessError(result.returncode, result.args, output=result.stdout, stderr=result.stderr)
match = re.search(r'request id is (\S+)', result.stdout)
if match:
print_id = match.group(1)
for file in files_path:
os.remove(file)
return Response({"code": return_code.SUCCESS, "print_id": print_id, })
return Response({"code": return_code.SUCCESS, "print_id": None, })
except CalledProcessError as e:
for file in files_path:
os.remove(file)
return Response({"code": return_code.ERROR, "error": f"{e.stderr}"})
else:
return Response({"code": return_code.ERROR, "error": serializer.errors})