Manga/Comic page layout

I’ve been trying to create a manga lines layout plugin, but I’m stuck. The lines are currently rendered on the canvas, and you can rotate the canvas, but when you zoom in, the lines become misaligned with the paper.

This is the test in a B6 (148x210 mm) 350dpi canvas (2039x2894 px), i also tested in a 600dpi and works.

The only problem is the zoom in/out, if there are a solution to this because would be a great step to add this function to Krita.



Here is the code, test it in the scripter:

Just for fun, I converted the script to tiny plugin + moved drawing to image coordinates. There is a docker but it has now content, maybe there should be some options on how the guides should be placed. Also where the lines are drawn is something that I did select randomly.

How to use:

  1. copy & paste install script to Krita’s scripter (toolsscriptsscripter)
  2. press run
  3. If scripter prints to output manga_guides_plugin added Successfully (Krita restart required.), install was success, else there is an error message telling what did go wrong.

Tested on Kubuntu Krita 6.1.0-prealpha (with luck works on 5.*.*)

from pathlib import Path
from krita import Krita

app = Krita.instance()
user_pykrita_path = Path(app.getAppDataLocation()) / 'pykrita'
plugin_desktop_path = user_pykrita_path / 'manga_guides_plugin.desktop'
plugin_dir = user_pykrita_path / 'manga_guides_plugin'
plugin_init_path = plugin_dir / '__init__.py'
manga_guides_docker_path = plugin_dir / 'manga_guides_docker.py'
manga_guides_overlay_path = plugin_dir / 'manga_guides_overlay.py'

plugin_dir.mkdir(parents=True)

plugin_desktop_path.write_text("""\
[Desktop Entry]
Type=Service
ServiceTypes=Krita/PythonPlugin
X-KDE-Library=manga_guides_plugin
X-Python-2-Compatible=false
X-Krita-Manual=readme.md
Name=Manga Guides Plugin
Comment=Manga Guides Plugin
""")

plugin_init_path.write_text("""\
PLUGIN_NAME = __name__
PLUGIN_NAME_SHORT = 'MAGP'  # (Ma)nga (g)uides (P)lugin
PLUGIN_VERSION = __version__ = '0.1.0'


def register_plugin():
    from .manga_guides_docker import MangaGuidesDocker
    MangaGuidesDocker.register()


register_plugin()
""")

manga_guides_docker_path.write_text("""\
# importing Qt stuff from krita module (Qt5 <-> Qt6 combability)
from krita import (
        Krita,
        DockWidget,
        DockWidgetFactoryBase,
        DockWidgetFactory,
        Qt,
        QMdiArea,
        QVBoxLayout,
        QWidget)

from manga_guides_plugin import (
        PLUGIN_NAME,
        PLUGIN_NAME_SHORT,
        PLUGIN_VERSION)

from .manga_guides_overlay import (
        MangaGuidesOverlay,)


_APP = Krita.instance()


def find_q_view(target_view):
    window = target_view.window()
    q_window = window.qwindow()
    mdi_area = q_window.findChild(QMdiArea)
    for q_view, view in zip(mdi_area.subWindowList(), window.views()):
        if view == target_view:
            return q_view
    raise RuntimeError('Something is wrong!')


def find_kis_canvases(parent):
    for widget in parent.findChildren(QWidget):
        if widget.inherits('KisCanvas2') or widget.inherits('KisOpenGLCanvas2'):
            yield widget


def find_kis_canvas_controllers(parent):
    for widget in parent.findChildren(QWidget):
        if widget.inherits('KisCanvasController'):
            yield widget


class MangaGuidesDocker(DockWidget):
    @classmethod
    def register(cls):
        factory = DockWidgetFactory(
                f'{PLUGIN_NAME}:manga_guides_docker',
                DockWidgetFactoryBase.DockPosition.DockRight,
                cls)
        _APP.addDockWidgetFactory(factory)

    def __init__(self):
        super().__init__()
        self.setWindowTitle('Manga Guides Docker')
        self._init_ui()

    def _init_ui(self):
        content = QWidget()
        layout = QVBoxLayout()
        content.setLayout(layout)
        self.setWidget(content)

    def canvasChanged(self, canvas):
        if not canvas or not canvas.view():
            return  # nothing to do

        q_view = find_q_view(canvas.view())
        kis_canvas_controller = next(find_kis_canvas_controllers(q_view), None)

        if kis_canvas_controller.findChild(MangaGuidesOverlay) is None:
            # missing a MangaGuidesOverlay -> adding one
            manga_guides_overlay = MangaGuidesOverlay(canvas=canvas, parent=kis_canvas_controller)
            kis_canvas_controller.installEventFilter(manga_guides_overlay)
            manga_guides_overlay.show()
            manga_guides_overlay.raise_()
""")

manga_guides_overlay_path.write_text("""\
# importing Qt stuff from krita module (Qt5 <-> Qt6 combability)
from krita import (
        Krita,
        Canvas,
        Qt,
        pyqtSlot as QSlot,
        pyqtSignal as QSignal,
        pyqtProperty as QProperty,
        QRectF,
        QPointF,
        QEvent,
        QPainter,
        QPen,
        QColor,
        QTransform,
        QWidget)


_APP = Krita.instance()


class MangaGuidesOverlay(QWidget):
    def __init__(self, **kwargs):
        self._canvas = None
        super().__init__(**kwargs)
        self.setAttribute(Qt.WidgetAttribute.WA_ForceDisabled, True)
        self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, True)
        self.setAutoFillBackground(False)

    @QProperty(Canvas)
    def canvas(self):
        return self._canvas

    @canvas.setter
    def canvas(self, new_canvas):
        self._canvas = new_canvas

    def eventFilter(self, obj, event):
        if event.type() in {QEvent.Type.Resize, QEvent.Type.LayoutRequest}:
            q_canvas = self.parent()
            self.setGeometry(q_canvas.rect())
        return super().eventFilter(obj, event)

    def widget_to_image_transform(self):
        view = self._canvas.view()
        to_canvas_transform = view.flakeToCanvasTransform()
        to_image_transform = view.flakeToImageTransform()
        inv_tr, _ = to_image_transform.inverted()
        transform = inv_tr * to_canvas_transform
        return transform

    def paintEvent(self, event):
        view = self._canvas.view()
        document = view.document()
        document_bounds = QRectF(document.bounds())
        document_center = document_bounds.center()

        painter = QPainter(self)
        painter.setRenderHint(QPainter.RenderHint.Antialiasing)

        # notes:
        #   - painter origin is placed at center of document.
        #   - units are in document pixels.

        transform = self.widget_to_image_transform()
        painter.setTransform(transform)

        debug_pen = QPen(QColor(255, 80, 80, 200), 5.0, Qt.PenStyle.SolidLine)
        painter.setPen(debug_pen)
        painter.drawRect(document_bounds)

        # notes:
        #   line width of 0 makes pen a "cosmetic" aka. "hairline" pen ->
        #   width of draw line is always one pixel on screen.

        pen_mark = QPen(QColor(0, 98, 255, 220), 0.0, Qt.PenStyle.SolidLine)
        pen_bleed = QPen(QColor(0, 98, 255, 220), 0.0, Qt.PenStyle.SolidLine)
        pen_safe = QPen(QColor(0, 98, 255, 220), 0.0, Qt.PenStyle.SolidLine)
        pen_trim = QPen(QColor(0, 98, 255, 220), 0.0, Qt.PenStyle.SolidLine)

        left_cross_center = QPointF(document_center)
        left_cross_center.setX(document_bounds.left() + 32.0)
        right_cross_center = QPointF(document_center)
        right_cross_center.setX(document_bounds.right() - 32.0)
        top_cross_center = QPointF(document_center)
        top_cross_center.setY(document_bounds.top() + 32.0)
        bottom_cross_center = QPointF(document_center)
        bottom_cross_center.setY(document_bounds.bottom() - 32.0)

        # Rectangles
        bleed_rect = document_bounds.adjusted(48.0, 48.0, -48.0, -48.0)
        trim_rect = document_bounds.adjusted(64.0, 64.0, -64.0, -64.0)
        safe_rect = document_bounds.adjusted(128.0, 128.0, -128.0, -128.0)

        painter.setPen(pen_mark)
        self.draw_cross(painter, left_cross_center, 32.0)
        self.draw_cross(painter, right_cross_center, 32.0)
        self.draw_cross(painter, top_cross_center, 32.0)
        self.draw_cross(painter, bottom_cross_center, 32.0)

        painter.setPen(pen_mark)
        painter.drawLine(
                QPointF(bleed_rect.left() + 32.0, bleed_rect.top()),
                QPointF(bleed_rect.left() + 32.0, bleed_rect.top() - 16.0))
        painter.drawLine(
                QPointF(bleed_rect.right() - 32.0, bleed_rect.top()),
                QPointF(bleed_rect.right() - 32.0, bleed_rect.top() - 16.0))
        painter.drawLine(
                QPointF(bleed_rect.left(), bleed_rect.top() + 32.0),
                QPointF(bleed_rect.left() - 16.0, bleed_rect.top() + 32.0))
        painter.drawLine(
                QPointF(bleed_rect.right(), bleed_rect.top() + 32.0),
                QPointF(bleed_rect.right() + 16.0, bleed_rect.top() + 32.0))

        painter.drawLine(
                QPointF(bleed_rect.left() + 32.0, bleed_rect.bottom()),
                QPointF(bleed_rect.left() + 32.0, bleed_rect.bottom() + 16.0))
        painter.drawLine(
                QPointF(bleed_rect.right() - 32.0, bleed_rect.bottom()),
                QPointF(bleed_rect.right() - 32.0, bleed_rect.bottom() + 16.0))
        painter.drawLine(
                QPointF(bleed_rect.left(), bleed_rect.bottom() - 32.0),
                QPointF(bleed_rect.left() - 16.0, bleed_rect.bottom() - 32.0))
        painter.drawLine(
                QPointF(bleed_rect.right(), bleed_rect.bottom() - 32.0),
                QPointF(bleed_rect.right() + 16.0, bleed_rect.bottom() - 32.0))

        painter.setPen(pen_bleed)
        painter.drawRect(bleed_rect)
        painter.setPen(pen_safe)
        painter.drawRect(safe_rect)
        painter.setPen(pen_trim)
        painter.drawRect(trim_rect)

        painter.end()

    def draw_cross(self, painter, center, width):
        half_width = 0.5 * width
        painter.drawLine(
                QPointF(center.x() - half_width, center.y()),
                QPointF(center.x() + half_width, center.y()))
        painter.drawLine(
                QPointF(center.x(), center.y() - half_width),
                QPointF(center.x(), center.y() + half_width))
""")

app.writeSetting('python', 'enable_manga_guides_plugin', 'true')
print('manga_guides_plugin added Successfully (Krita restart required.)')