sammycage / lunasvg

SVG rendering and manipulation library in C++
MIT License
866 stars 124 forks source link

SVG image rotation #67

Closed viktoras-pal closed 2 years ago

viktoras-pal commented 2 years ago

How to rotate a final SVG image so that it would be rendered correctly without cropping? It would be nice to have a method to render a rotated and scaled SVG image, like this:

Bitmap Document::renderToBitmapRotated(double angle, double scale, std::uint32_t backgroundColor) const;

sammycage commented 2 years ago
<svg id="svg1" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">

    <rect id="rect1" x="20" y="20" width="160" height="160" fill="red"/>

</svg>

Original

rect svg

document->rotate(45)

rect-rotated svg

document->rotate(45, document->width() / 2.0, document->heigth() / 2.0)

rect svg

How to rotate a final SVG image so that it would be rendered correctly without cropping?

Is this what you meant by cropping?

viktoras-pal commented 2 years ago

Yes. The result should be a full square (not clipped with viewBox) : image I have a collection of SVG map symbols and I want to draw them on a map at different rotation angles.

CSS transformation functions Rotate()/Scale() work similarly as needed:

<!DOCTYPE html>
<html>
<body>
<div style="transform:rotate(45deg); display:inline-block; margin:40px; background:yellow;">
   <svg id="svg1" width="200" height="200" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
       <rect id="rect1" x="20" y="20" width="160" height="160" fill="red"/>
   </svg>
</div>
<div style="transform:rotate(45deg) scale(0.5); display:inline-block; margin:40px; background:yellow;">
   <svg id="svg2" width="200" height="200" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
       <rect id="rect1" x="20" y="20" width="160" height="160" fill="red"/>
</div>
</body>
</html>

image

sammycage commented 2 years ago
Bitmap renderToBitmapRotated(Document* document, double angle, double scale, std::uint32_t bgColor)
{
    const double pi = 3.14159265358979323846;

    double radians = pi * angle / 180.0;
    double sine = std::sin(radians);
    double cosine = std::cos(radians);

    double originalWidth = document->width();
    double originalHeight = document->height();

    double rotatedWidth = cosine * originalWidth + sine * originalHeight;
    double rotatedHeight = cosine * originalHeight + sine * originalWidth;

    document->scale(originalWidth / rotatedWidth, originalHeight / rotatedHeight);

    document->translate(rotatedWidth / 2.0, rotatedHeight / 2.0);
    document->rotate(angle);
    document->translate(-originalWidth / 2.0, -originalHeight / 2.0);

    document->scale(scale, scale);

    Bitmap bitmap(originalWidth, originalHeight);
    document->render(bitmap, {}, bgColor);
    return bitmap;
}

Lemme know if it works :)

viktoras-pal commented 2 years ago

Thank you. I changed Document* to std::unique_ptr& because there was a compiler error.

I tested it, but the result is not quite what it should be. When scale is 1, the resulting square is smaller than the original. Another problem is that if the viewBox is not square, the resulting shape is skewed:

<svg viewBox="0 0 240 140" xmlns="http://www.w3.org/2000/svg">
<rect x="20" y="20" width="100" height="100" style="fill:red;" />
</svg>

Original:
curves svg After rotation: curves svg1

sammycage commented 2 years ago

When scale is 1, the resulting square is smaller than the original. Another problem is that if the viewBox is not square, the resulting shape is skewed:

When rotating an image the bounding box changes so I have to scale the whole image back to its original size.

Please try this one with the latest commit 8fafd4398a220a696e6e6fc4eff35eb05d570ea7

Bitmap renderToBitmapRotated(Document* document, double angle, double scale, std::uint32_t bgColor)
{
    const double pi = 3.14159265358979323846;

    double radians = pi * angle / 180.0;
    double sine = std::sin(radians);
    double cosine = std::cos(radians);

    double originalWidth = document->width();
    double originalHeight = document->height();

    double rotatedWidth = cosine * originalWidth + sine * originalHeight;
    double rotatedHeight = cosine * originalHeight + sine * originalWidth;

    Matrix matrix;
    //matrix.scale(originalWidth / rotatedWidth, originalHeight / rotatedHeight);
    matrix.translate(rotatedWidth / 2.0, rotatedHeight / 2.0);
    matrix.rotate(angle);
    matrix.translate(-originalWidth / 2.0, -originalHeight / 2.0);
    matrix.scale(scale, scale);

    Bitmap bitmap(rotatedWidth, rotatedWidth);
    document->render(bitmap, matrix, bgColor);
    return bitmap;
}

N.B the resulting image might be larger than the original image

viktoras-pal commented 2 years ago

I made some corrections:

Bitmap renderToBitmapRotated(const Document* document, double angle, double scale, std::uint32_t bgColor)
{
    const double pi = 3.14159265358979323846;

    double radians = pi * angle / 180.0;
    double sine = std::abs(std::sin(radians));
    double cosine = std::abs(std::cos(radians));

    double originalWidth = document->width();
    double originalHeight = document->height();

    double rotatedWidth = cosine * originalWidth + sine * originalHeight;
    double rotatedHeight = cosine * originalHeight + sine * originalWidth;

    Matrix matrix;
    matrix.translate(rotatedWidth / 2.0, rotatedHeight / 2.0);
    matrix.rotate(angle);
    matrix.translate(-originalWidth / 2.0, -originalHeight / 2.0);
    matrix.scale(scale, scale);

    std::uint32_t width = static_cast<std::uint32_t>(std::ceil(rotatedWidth));
    std::uint32_t height = static_cast<std::uint32_t>(std::ceil(rotatedHeight));

    Bitmap bitmap(width, height);
    document->render(bitmap, matrix, bgColor);
    return bitmap;
}

Now everything works fine except when viewBox is undefined.

viktoras-pal commented 2 years ago

I haven’t tried scaling before. If the scale is not 1, it renders incorrectly.

sammycage commented 2 years ago

Bitmap renderToBitmapRotated(Document* document, double angle, double scale, std::uint32_t bgColor)
{
    const double pi = 3.14159265358979323846;

    double radians = pi * angle / 180.0;
    double sine = std::abs(std::sin(radians));
    double cosine = std::abs(std::cos(radians));

    double originalWidth = document->width();
    double originalHeight = document->height();

    double rotatedWidth = cosine * originalWidth + sine * originalHeight;
    double rotatedHeight = cosine * originalHeight + sine * originalWidth;

    double scaledWidth = rotatedWidth * scale;
    double scaledHeight = rotatedHeight * scale;

    Matrix matrix;
    matrix.scale(scaledWidth / rotatedWidth, scaledHeight / rotatedHeight);
    matrix.translate(rotatedWidth / 2.0, rotatedHeight / 2.0);
    matrix.rotate(angle);
    matrix.translate(-originalWidth / 2.0, -originalHeight / 2.0);

    std::uint32_t width = static_cast<std::uint32_t>(std::ceil(scaledWidth));
    std::uint32_t height = static_cast<std::uint32_t>(std::ceil(scaledHeight));

    Bitmap bitmap(width, height);
    document->render(bitmap, matrix, bgColor);
    return bitmap;
}

I hope it works

viktoras-pal commented 2 years ago

Thank you, it works as expected now. Example:

<svg  width="240" height="140" viewBox="100 0 240 140" xmlns="http://www.w3.org/2000/svg">
<rect x="110" y="10" width="220" height="120" style="fill:red;"  stroke="blue" stroke-width="1" />
</svg>

Original: curves svg Scale: 0.5, rotation angles: 0/30/60/90/120 curves svg00 curves svg30 curves svg60 curves svg90 curves svg120

You could include this useful function to your library as a method of Document class.

sammycage commented 2 years ago

Thank you for opening this issue <3 <3 <3