ComfyUI Node

QBB_PenTool

QBB_PenTool

By rhinoflavored·Created about a month ago·Updated about a month ago· 0
QBB_PenTool
  • image
  • image_2
  • image_3
  • image_4
  • image_5
  • image_6
  • image_7
  • image_8
  • image_9
  • image_10
  • image_11
  • image_12
  • image_13
  • image_14
  • image_15
  • image_16
  • image
  • mask
  • stroke_image
  • image_alpha
  • path_json
path_data{"version":2,"canvas":{"width":1024,"height":1024,"backgroundColor":"#FFFFFF","backgroundFillMode":"none"},"styles":{"maskFeather":0},"strokePaths":[],"selectionPaths":[],"imageLayers":[]}

ComfyUI can detect, upscale, and inpaint, but it has no built-in way to just sit down and draw on an image. That's the whole gap QBB_PenTool fills: a vector pen editor that lives inside the node. Click a button, a modal canvas opens, and you draw paths, Bezier curves, shapes, text, and freehand brush strokes over your base image - or a blank canvas - and the node hands you back a composite, a mask, a transparent stroke layer, and the path data as JSON.

It's a niche thing and it's honest about being one. But every masked-inpainting workflow has a moment where a detector gives you the wrong region and you think "I could draw this myself in ten seconds." This is the ten-second tool, and unlike a round-trip through Photoshop it never leaves the graph. Most masks in this ecosystem are obtained - YOLO boxes, GroundingDINO + SAM, an ADetailer pass - but hand-drawn is the forgotten option, and it's the one with exact placement: the boundary is whatever you drew, nothing more. That still has real value in the instruction-edit era, because masked inpainting is the one thing that guarantees unmasked pixels stay bit-identical.

How it works

The whole document lives in a single hidden path_data STRING input: a v2 JSON blob carrying the canvas size and background, the styles (like maskFeather), stroke paths, selection paths, image layers, text objects, and brush strokes. The Python side never sees you drawing in real time. It just rasterizes whatever JSON is sitting in path_data when the prompt runs - the frontend JavaScript extension is what opens the editor and writes your drawing back into that field when you queue.

Two mechanism details are worth knowing before you wire anything. First, canvas size always comes from the document, not from your input image. Feed it a 1024×768 render and the canvas is whatever the editor says, with your image composited on top as a layer. Second, the rasterization is real vector rendering - segments follow the SVG-ish M/L/C (cubic Bezier) command set, so curves stay smooth at any resolution.

The inputs and outputs that matter

For a beginner there's exactly one input to set: image. It's optional - leave it disconnected for a blank canvas. Connect a single image or a whole batch, and connecting one auto-adds image_2 through image_16 slots so you can layer several inputs in the editor. Everything else - canvas size, background (solid/linear/radial gradient), stroke width, mask feather, brush choice - is set inside the editor, not on the node. path_data itself is hidden and editor-maintained; don't hand-edit it.

The five outputs, and what you wire them into:

  • image - the RGB composite (base + strokes), for preview or straight to Save.
  • mask - the union of your closed selection paths. All black if you drew no closed selection. This is the one that feeds SetLatentNoiseMask for inpainting.
  • stroke_image - the pure stroke/paint layer on a transparent background (RGBA). Great for compositing strokes over something else.
  • image_alpha - the composite with the mask used as alpha, i.e. everything outside your selection transparent.
  • path_json - the v2 path JSON for this exact rasterization, if you want to reuse the drawing programmatically.

Installing it

Zero-dependency install, which is rare enough to be worth calling out - the requirements file is literally commented out because torch, numpy, and Pillow are already in every ComfyUI environment. The README lists requires-comfyui >= 0.3.0, so keep ComfyUI updated.

ComfyUI Manager → search QBB PenTool, or from a terminal:

comfy node install qbb-pentool

or clone it by hand:

cd ComfyUI/custom_nodes
git clone https://github.com/rhinoflavored/ComfyUI_QBB_PenTool

Then restart ComfyUI completely. If you ever installed it under an older folder name (comfyui_QBB_penTool or penTool), delete those folders first - the README is explicit that leftover copies cause duplicate-load or node-conflict errors.

Gotchas that will trip you

The big one, shouted from the README: queuing a prompt does not pop the editor open. It's a frontend tool, so you open it manually - the "Open Pen Editor" button on the node, double-click the node, or right-click → open pen editor. If nothing happens, you haven't broken the workflow; you've just missed the button.

The RGBA trap comes next. stroke_image and image_alpha are 4-channel, and a lot of downstream nodes only accept 3-channel RGB - split out the alpha first or you'll get a type error that looks unrelated. And since the pack is brand new (essentially zero community footprint), expect small rough edges; the README's troubleshooting section is unusually thorough for a reason.

If you've changed the JS, hard-refresh the browser (Ctrl+F5); Python changes need a restart. The console line [ComfyUI_QBB_PenTool] frontend extension loaded is your proof the frontend actually mounted.

CategoryQBB/ComfyUI_QBB_PenTool

Inputs (17)

NameTypeDefaultDescription
path_dataSTRING{"version":2,"canvas":{"width":1024,"height":1024,"backgroundColor":"#FFFFFF","backgroundFillMode":"none"},"styles":{"maskFeather":0},"strokePaths":[],"selectionPaths":[],"imageLayers":[]}Internal path data maintained by the Pen Editor (hidden in the node UI)
imageoptIMAGEOptional. Single or batch IMAGE; connecting one adds the next image slot. Multiple inputs merge into editor layers (or use one batch input).
image_2optIMAGEOptional image slot 2 (batch OK)
image_3optIMAGEOptional image slot 3 (batch OK)
image_4optIMAGEOptional image slot 4 (batch OK)
image_5optIMAGEOptional image slot 5 (batch OK)
image_6optIMAGEOptional image slot 6 (batch OK)
image_7optIMAGEOptional image slot 7 (batch OK)
image_8optIMAGEOptional image slot 8 (batch OK)
image_9optIMAGEOptional image slot 9 (batch OK)
image_10optIMAGEOptional image slot 10 (batch OK)
image_11optIMAGEOptional image slot 11 (batch OK)
image_12optIMAGEOptional image slot 12 (batch OK)
image_13optIMAGEOptional image slot 13 (batch OK)
image_14optIMAGEOptional image slot 14 (batch OK)
image_15optIMAGEOptional image slot 15 (batch OK)
image_16optIMAGEOptional image slot 16 (batch OK)

Outputs (5)

NameTypeDescription
imageIMAGE
maskMASK
stroke_imageIMAGE
image_alphaIMAGE
path_jsonSTRING