diff mupdf-source/thirdparty/leptonica/src/rotate.c @ 2:b50eed0cc0ef upstream

ADD: MuPDF v1.26.7: the MuPDF source as downloaded by a default build of PyMuPDF 1.26.4. The directory name has changed: no version number in the expanded directory now.
author Franz Glasner <fzglas.hg@dom66.de>
date Mon, 15 Sep 2025 11:43:07 +0200
parents
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mupdf-source/thirdparty/leptonica/src/rotate.c	Mon Sep 15 11:43:07 2025 +0200
@@ -0,0 +1,588 @@
+/*====================================================================*
+ -  Copyright (C) 2001 Leptonica.  All rights reserved.
+ -
+ -  Redistribution and use in source and binary forms, with or without
+ -  modification, are permitted provided that the following conditions
+ -  are met:
+ -  1. Redistributions of source code must retain the above copyright
+ -     notice, this list of conditions and the following disclaimer.
+ -  2. Redistributions in binary form must reproduce the above
+ -     copyright notice, this list of conditions and the following
+ -     disclaimer in the documentation and/or other materials
+ -     provided with the distribution.
+ -
+ -  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ -  ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ -  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ -  A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL ANY
+ -  CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ -  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ -  PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ -  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ -  OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ -  NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ -  SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *====================================================================*/
+
+/*!
+ * \file rotate.c
+ * <pre>
+ *
+ *     General rotation about image center
+ *              PIX     *pixRotate()
+ *              PIX     *pixEmbedForRotation()
+ *
+ *     General rotation by sampling
+ *              PIX     *pixRotateBySampling()
+ *
+ *     Nice (slow) rotation of 1 bpp image
+ *              PIX     *pixRotateBinaryNice()
+ *
+ *     Rotation including alpha (blend) component
+ *              PIX     *pixRotateWithAlpha()
+ *
+ *     Rotations are measured in radians; clockwise is positive.
+ *
+ *     The general rotation pixRotate() does the best job for
+ *     rotating about the image center.  For 1 bpp, it uses shear;
+ *     for others, it uses either shear or area mapping.
+ *     If requested, it expands the output image so that no pixels are lost
+ *     in the rotation, and this can be done on multiple successive shears
+ *     without expanding beyond the maximum necessary size.
+ * </pre>
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config_auto.h>
+#endif  /* HAVE_CONFIG_H */
+
+#include <math.h>
+#include "allheaders.h"
+
+extern l_float32  AlphaMaskBorderVals[2];
+static const l_float32  MinAngleToRotate = 0.001f;  /* radians; ~0.06 deg */
+static const l_float32  Max1BppShearAngle = 0.06f;  /* radians; ~3 deg    */
+static const l_float32  LimitShearAngle = 0.35f;    /* radians; ~20 deg   */
+
+/*------------------------------------------------------------------*
+ *                  General rotation about the center               *
+ *------------------------------------------------------------------*/
+/*!
+ * \brief   pixRotate()
+ *
+ * \param[in]    pixs     1, 2, 4, 8, 32 bpp rgb
+ * \param[in]    angle    radians; clockwise is positive
+ * \param[in]    type     L_ROTATE_AREA_MAP, L_ROTATE_SHEAR, L_ROTATE_SAMPLING
+ * \param[in]    incolor  L_BRING_IN_WHITE, L_BRING_IN_BLACK
+ * \param[in]    width    original width; use 0 to avoid embedding
+ * \param[in]    height   original height; use 0 to avoid embedding
+ * \return  pixd, or NULL on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) This is a high-level, simple interface for rotating images
+ *          about their center.
+ *      (2) For very small rotations, just return a clone.
+ *      (3) Rotation brings either white or black pixels in
+ *          from outside the image.
+ *      (4) The rotation type is adjusted if necessary for the image
+ *          depth and size of rotation angle.  For 1 bpp images, we
+ *          rotate either by shear or sampling.
+ *      (5) Colormaps are removed for rotation by area mapping.
+ *      (6) The dest can be expanded so that no image pixels
+ *          are lost.  To invoke expansion, input the original
+ *          width and height.  For repeated rotation, use of the
+ *          original width and height allows the expansion to
+ *          stop at the maximum required size, which is a square
+ *          with side = sqrt(w*w + h*h).
+ * </pre>
+ */
+PIX *
+pixRotate(PIX       *pixs,
+          l_float32  angle,
+          l_int32    type,
+          l_int32    incolor,
+          l_int32    width,
+          l_int32    height)
+{
+l_int32    w, h, d;
+l_uint32   fillval;
+PIX       *pix1, *pix2, *pix3, *pixd;
+PIXCMAP   *cmap;
+
+    if (!pixs)
+        return (PIX *)ERROR_PTR("pixs not defined", __func__, NULL);
+    if (type != L_ROTATE_SHEAR && type != L_ROTATE_AREA_MAP &&
+        type != L_ROTATE_SAMPLING)
+        return (PIX *)ERROR_PTR("invalid type", __func__, NULL);
+    if (incolor != L_BRING_IN_WHITE && incolor != L_BRING_IN_BLACK)
+        return (PIX *)ERROR_PTR("invalid incolor", __func__, NULL);
+
+    if (L_ABS(angle) < MinAngleToRotate)
+        return pixClone(pixs);
+
+        /* Adjust rotation type if necessary:
+         *  - If d == 1 bpp and the angle is more than about 6 degrees,
+         *    rotate by sampling; otherwise rotate by shear.
+         *  - If d > 1, only allow shear rotation up to about 20 degrees;
+         *    beyond that, default a shear request to sampling. */
+    if (pixGetDepth(pixs) == 1) {
+        if (L_ABS(angle) > Max1BppShearAngle) {
+            if (type != L_ROTATE_SAMPLING)
+                L_INFO("1 bpp, large angle; rotate by sampling\n", __func__);
+            type = L_ROTATE_SAMPLING;
+        } else if (type != L_ROTATE_SHEAR) {
+            L_INFO("1 bpp; rotate by shear\n", __func__);
+            type = L_ROTATE_SHEAR;
+        }
+    } else if (L_ABS(angle) > LimitShearAngle && type == L_ROTATE_SHEAR) {
+        L_INFO("large angle; rotate by sampling\n", __func__);
+        type = L_ROTATE_SAMPLING;
+    }
+
+        /* Remove colormap if we rotate by area mapping. */
+    cmap = pixGetColormap(pixs);
+    if (cmap && type == L_ROTATE_AREA_MAP)
+        pix1 = pixRemoveColormap(pixs, REMOVE_CMAP_BASED_ON_SRC);
+    else
+        pix1 = pixClone(pixs);
+    cmap = pixGetColormap(pix1);
+
+        /* Otherwise, if there is a colormap and we're not embedding,
+         * add white color if it doesn't exist. */
+    if (cmap && width == 0) {  /* no embedding; generate %incolor */
+        if (incolor == L_BRING_IN_BLACK)
+            pixcmapAddBlackOrWhite(cmap, 0, NULL);
+        else  /* L_BRING_IN_WHITE */
+            pixcmapAddBlackOrWhite(cmap, 1, NULL);
+    }
+
+        /* Request to embed in a larger image; do if necessary */
+    pix2 = pixEmbedForRotation(pix1, angle, incolor, width, height);
+
+        /* Area mapping requires 8 or 32 bpp.  If less than 8 bpp and
+         * area map rotation is requested, convert to 8 bpp. */
+    d = pixGetDepth(pix2);
+    if (type == L_ROTATE_AREA_MAP && d < 8)
+        pix3 = pixConvertTo8(pix2, FALSE);
+    else
+        pix3 = pixClone(pix2);
+
+        /* Do the rotation: shear, sampling or area mapping */
+    pixGetDimensions(pix3, &w, &h, &d);
+    if (type == L_ROTATE_SHEAR) {
+        pixd = pixRotateShearCenter(pix3, angle, incolor);
+    } else if (type == L_ROTATE_SAMPLING) {
+        pixd = pixRotateBySampling(pix3, w / 2, h / 2, angle, incolor);
+    } else {  /* rotate by area mapping */
+        fillval = 0;
+        if (incolor == L_BRING_IN_WHITE) {
+            if (d == 8)
+                fillval = 255;
+            else  /* d == 32 */
+                fillval = 0xffffff00;
+        }
+        if (d == 8)
+            pixd = pixRotateAMGray(pix3, angle, fillval);
+        else  /* d == 32 */
+            pixd = pixRotateAMColor(pix3, angle, fillval);
+    }
+
+    pixDestroy(&pix1);
+    pixDestroy(&pix2);
+    pixDestroy(&pix3);
+    return pixd;
+}
+
+
+/*!
+ * \brief   pixEmbedForRotation()
+ *
+ * \param[in]    pixs      1, 2, 4, 8, 32 bpp rgb
+ * \param[in]    angle     radians; clockwise is positive
+ * \param[in]    incolor   L_BRING_IN_WHITE, L_BRING_IN_BLACK
+ * \param[in]    width     original width; use 0 to avoid embedding
+ * \param[in]    height    original height; use 0 to avoid embedding
+ * \return  pixd, or NULL on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) For very small rotations, just return a clone.
+ *      (2) Generate larger image to embed pixs if necessary, and
+ *          place the center of the input image in the center.
+ *      (3) Rotation brings either white or black pixels in
+ *          from outside the image.  For colormapped images where
+ *          there is no white or black, a new color is added if
+ *          possible for these pixels; otherwise, either the
+ *          lightest or darkest color is used.  In most cases,
+ *          the colormap will be removed prior to rotation.
+ *      (4) The dest is to be expanded so that no image pixels
+ *          are lost after rotation.  Input of the original width
+ *          and height allows the expansion to stop at the maximum
+ *          required size, which is a square with side equal to
+ *          sqrt(w*w + h*h).
+ *      (5) For an arbitrary angle, the expansion can be found by
+ *          considering the UL and UR corners.  As the image is
+ *          rotated, these move in an arc centered at the center of
+ *          the image.  Normalize to a unit circle by dividing by half
+ *          the image diagonal.  After a rotation of T radians, the UL
+ *          and UR corners are at points T radians along the unit
+ *          circle.  Compute the x and y coordinates of both these
+ *          points and take the max of absolute values; these represent
+ *          the half width and half height of the containing rectangle.
+ *          The arithmetic is done using formulas for sin(a+b) and cos(a+b),
+ *          where b = T.  For the UR corner, sin(a) = h/d and cos(a) = w/d.
+ *          For the UL corner, replace a by (pi - a), and you have
+ *          sin(pi - a) = h/d, cos(pi - a) = -w/d.  The equations
+ *          given below follow directly.
+ * </pre>
+ */
+PIX *
+pixEmbedForRotation(PIX       *pixs,
+                    l_float32  angle,
+                    l_int32    incolor,
+                    l_int32    width,
+                    l_int32    height)
+{
+l_int32    w, h, d, w1, h1, w2, h2, maxside, wnew, hnew, xoff, yoff, setcolor;
+l_float64  sina, cosa, fw, fh;
+PIX       *pixd;
+
+    if (!pixs)
+        return (PIX *)ERROR_PTR("pixs not defined", __func__, NULL);
+    if (incolor != L_BRING_IN_WHITE && incolor != L_BRING_IN_BLACK)
+        return (PIX *)ERROR_PTR("invalid incolor", __func__, NULL);
+    if (L_ABS(angle) < MinAngleToRotate)
+        return pixClone(pixs);
+
+        /* Test if big enough to hold any rotation of the original image */
+    pixGetDimensions(pixs, &w, &h, &d);
+    maxside = (l_int32)(sqrt((l_float64)(width * width) +
+                             (l_float64)(height * height)) + 0.5);
+    if (w >= maxside && h >= maxside)  /* big enough */
+        return pixClone(pixs);
+
+        /* Find the new sizes required to hold the image after rotation.
+         * Note that the new dimensions must be at least as large as those
+         * of pixs, because we're rasterop-ing into it before rotation. */
+    cosa = cos(angle);
+    sina = sin(angle);
+    fw = (l_float64)w;
+    fh = (l_float64)h;
+    w1 = (l_int32)(L_ABS(fw * cosa - fh * sina) + 0.5);
+    w2 = (l_int32)(L_ABS(-fw * cosa - fh * sina) + 0.5);
+    h1 = (l_int32)(L_ABS(fw * sina + fh * cosa) + 0.5);
+    h2 = (l_int32)(L_ABS(-fw * sina + fh * cosa) + 0.5);
+    wnew = L_MAX(w, L_MAX(w1, w2));
+    hnew = L_MAX(h, L_MAX(h1, h2));
+
+    if ((pixd = pixCreate(wnew, hnew, d)) == NULL)
+        return (PIX *)ERROR_PTR("pixd not made", __func__, NULL);
+    pixCopyResolution(pixd, pixs);
+    pixCopyColormap(pixd, pixs);
+    pixCopySpp(pixd, pixs);
+    pixCopyText(pixd, pixs);
+    xoff = (wnew - w) / 2;
+    yoff = (hnew - h) / 2;
+
+        /* Set background to color to be rotated in */
+    setcolor = (incolor == L_BRING_IN_BLACK) ? L_SET_BLACK : L_SET_WHITE;
+    pixSetBlackOrWhite(pixd, setcolor);
+
+        /* Rasterop automatically handles all 4 channels for rgba */
+    pixRasterop(pixd, xoff, yoff, w, h, PIX_SRC, pixs, 0, 0);
+    return pixd;
+}
+
+
+/*------------------------------------------------------------------*
+ *                    General rotation by sampling                  *
+ *------------------------------------------------------------------*/
+/*!
+ * \brief   pixRotateBySampling()
+ *
+ * \param[in]    pixs     1, 2, 4, 8, 16, 32 bpp rgb; can be cmapped
+ * \param[in]    xcen     x value of center of rotation
+ * \param[in]    ycen     y value of center of rotation
+ * \param[in]    angle    radians; clockwise is positive
+ * \param[in]    incolor  L_BRING_IN_WHITE, L_BRING_IN_BLACK
+ * \return  pixd, or NULL on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) For very small rotations, just return a clone.
+ *      (2) Rotation brings either white or black pixels in
+ *          from outside the image.
+ *      (3) Colormaps are retained.
+ * </pre>
+ */
+PIX *
+pixRotateBySampling(PIX       *pixs,
+                    l_int32    xcen,
+                    l_int32    ycen,
+                    l_float32  angle,
+                    l_int32    incolor)
+{
+l_int32    w, h, d, i, j, x, y, xdif, ydif, wm1, hm1, wpld;
+l_uint32   val;
+l_float32  sina, cosa;
+l_uint32  *datad, *lined;
+void     **lines;
+PIX       *pixd;
+
+    if (!pixs)
+        return (PIX *)ERROR_PTR("pixs not defined", __func__, NULL);
+    if (incolor != L_BRING_IN_WHITE && incolor != L_BRING_IN_BLACK)
+        return (PIX *)ERROR_PTR("invalid incolor", __func__, NULL);
+    pixGetDimensions(pixs, &w, &h, &d);
+    if (d != 1 && d != 2 && d != 4 && d != 8 && d != 16 && d != 32)
+        return (PIX *)ERROR_PTR("invalid depth", __func__, NULL);
+
+    if (L_ABS(angle) < MinAngleToRotate)
+        return pixClone(pixs);
+
+    if ((pixd = pixCreateTemplate(pixs)) == NULL)
+        return (PIX *)ERROR_PTR("pixd not made", __func__, NULL);
+    pixSetBlackOrWhite(pixd, incolor);
+
+    sina = sin(angle);
+    cosa = cos(angle);
+    datad = pixGetData(pixd);
+    wpld = pixGetWpl(pixd);
+    wm1 = w - 1;
+    hm1 = h - 1;
+    lines = pixGetLinePtrs(pixs, NULL);
+
+        /* Treat 1 bpp case specially */
+    if (d == 1) {
+        for (i = 0; i < h; i++) {  /* scan over pixd */
+            lined = datad + i * wpld;
+            ydif = ycen - i;
+            for (j = 0; j < w; j++) {
+                xdif = xcen - j;
+                x = xcen + (l_int32)(-xdif * cosa - ydif * sina);
+                if (x < 0 || x > wm1) continue;
+                y = ycen + (l_int32)(-ydif * cosa + xdif * sina);
+                if (y < 0 || y > hm1) continue;
+                if (incolor == L_BRING_IN_WHITE) {
+                    if (GET_DATA_BIT(lines[y], x))
+                        SET_DATA_BIT(lined, j);
+                } else {
+                    if (!GET_DATA_BIT(lines[y], x))
+                        CLEAR_DATA_BIT(lined, j);
+                }
+            }
+        }
+        LEPT_FREE(lines);
+        return pixd;
+    }
+
+    for (i = 0; i < h; i++) {  /* scan over pixd */
+        lined = datad + i * wpld;
+        ydif = ycen - i;
+        for (j = 0; j < w; j++) {
+            xdif = xcen - j;
+            x = xcen + (l_int32)(-xdif * cosa - ydif * sina);
+            if (x < 0 || x > wm1) continue;
+            y = ycen + (l_int32)(-ydif * cosa + xdif * sina);
+            if (y < 0 || y > hm1) continue;
+            switch (d)
+            {
+            case 8:
+                val = GET_DATA_BYTE(lines[y], x);
+                SET_DATA_BYTE(lined, j, val);
+                break;
+            case 32:
+                val = GET_DATA_FOUR_BYTES(lines[y], x);
+                SET_DATA_FOUR_BYTES(lined, j, val);
+                break;
+            case 2:
+                val = GET_DATA_DIBIT(lines[y], x);
+                SET_DATA_DIBIT(lined, j, val);
+                break;
+            case 4:
+                val = GET_DATA_QBIT(lines[y], x);
+                SET_DATA_QBIT(lined, j, val);
+                break;
+            case 16:
+                val = GET_DATA_TWO_BYTES(lines[y], x);
+                SET_DATA_TWO_BYTES(lined, j, val);
+                break;
+            default:
+                return (PIX *)ERROR_PTR("invalid depth", __func__, NULL);
+            }
+        }
+    }
+
+    LEPT_FREE(lines);
+    return pixd;
+}
+
+
+/*------------------------------------------------------------------*
+ *                 Nice (slow) rotation of 1 bpp image              *
+ *------------------------------------------------------------------*/
+/*!
+ * \brief   pixRotateBinaryNice()
+ *
+ * \param[in]    pixs     1 bpp
+ * \param[in]    angle    radians; clockwise is positive; about the center
+ * \param[in]    incolor  L_BRING_IN_WHITE, L_BRING_IN_BLACK
+ * \return  pixd, or NULL on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) For very small rotations, just return a clone.
+ *      (2) This does a computationally expensive rotation of 1 bpp images.
+ *          The fastest rotators (using shears or subsampling) leave
+ *          visible horizontal and vertical shear lines across which
+ *          the image shear changes by one pixel.  To ameliorate the
+ *          visual effect one can introduce random dithering.  One
+ *          way to do this in a not-too-random fashion is given here.
+ *          We convert to 8 bpp, do a very small blur, rotate using
+ *          linear interpolation (same as area mapping), do a
+ *          small amount of sharpening to compensate for the initial
+ *          blur, and threshold back to binary.  The shear lines
+ *          are magically removed.
+ *      (3) This operation is about 5x slower than rotation by sampling.
+ * </pre>
+ */
+PIX *
+pixRotateBinaryNice(PIX       *pixs,
+                    l_float32  angle,
+                    l_int32    incolor)
+{
+PIX  *pix1, *pix2, *pix3, *pix4, *pixd;
+
+    if (!pixs || pixGetDepth(pixs) != 1)
+        return (PIX *)ERROR_PTR("pixs undefined or not 1 bpp", __func__, NULL);
+    if (incolor != L_BRING_IN_WHITE && incolor != L_BRING_IN_BLACK)
+        return (PIX *)ERROR_PTR("invalid incolor", __func__, NULL);
+
+    pix1 = pixConvertTo8(pixs, 0);
+    pix2 = pixBlockconv(pix1, 1, 1);  /* smallest blur allowed */
+    pix3 = pixRotateAM(pix2, angle, incolor);
+    pix4 = pixUnsharpMasking(pix3, 1, 1.0);  /* sharpen a bit */
+    pixd = pixThresholdToBinary(pix4, 128);
+    pixDestroy(&pix1);
+    pixDestroy(&pix2);
+    pixDestroy(&pix3);
+    pixDestroy(&pix4);
+    return pixd;
+}
+
+
+/*------------------------------------------------------------------*
+ *             Rotation including alpha (blend) component           *
+ *------------------------------------------------------------------*/
+/*!
+ * \brief   pixRotateWithAlpha()
+ *
+ * \param[in]    pixs     32 bpp rgb or cmapped
+ * \param[in]    angle    radians; clockwise is positive
+ * \param[in]    pixg     [optional] 8 bpp, can be null
+ * \param[in]    fract    between 0.0 and 1.0, with 0.0 fully transparent
+ *                        and 1.0 fully opaque
+ * \return  pixd 32 bpp rgba, or NULL on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) The alpha channel is transformed separately from pixs,
+ *          and aligns with it, being fully transparent outside the
+ *          boundary of the transformed pixs.  For pixels that are fully
+ *          transparent, a blending function like pixBlendWithGrayMask()
+ *          will give zero weight to corresponding pixels in pixs.
+ *      (2) Rotation is about the center of the image; for very small
+ *          rotations, just return a clone.  The dest is automatically
+ *          expanded so that no image pixels are lost.
+ *      (3) Rotation is by area mapping.  It doesn't matter what
+ *          color is brought in because the alpha channel will
+ *          be transparent (black) there.
+ *      (4) If pixg is NULL, it is generated as an alpha layer that is
+ *          partially opaque, using %fract.  Otherwise, it is cropped
+ *          to pixs if required and %fract is ignored.  The alpha
+ *          channel in pixs is never used.
+ *      (4) Colormaps are removed to 32 bpp.
+ *      (5) The default setting for the border values in the alpha channel
+ *          is 0 (transparent) for the outermost ring of pixels and
+ *          (0.5 * fract * 255) for the second ring.  When blended over
+ *          a second image, this
+ *          (a) shrinks the visible image to make a clean overlap edge
+ *              with an image below, and
+ *          (b) softens the edges by weakening the aliasing there.
+ *          Use l_setAlphaMaskBorder() to change these values.
+ *      (6) A subtle use of gamma correction is to remove gamma correction
+ *          before rotation and restore it afterwards.  This is done
+ *          by sandwiching this function between a gamma/inverse-gamma
+ *          photometric transform:
+ *              pixt = pixGammaTRCWithAlpha(NULL, pixs, 1.0 / gamma, 0, 255);
+ *              pixd = pixRotateWithAlpha(pixt, angle, NULL, fract);
+ *              pixGammaTRCWithAlpha(pixd, pixd, gamma, 0, 255);
+ *              pixDestroy(&pixt);
+ *          This has the side-effect of producing artifacts in the very
+ *          dark regions.
+ * </pre>
+ */
+PIX *
+pixRotateWithAlpha(PIX       *pixs,
+                   l_float32  angle,
+                   PIX       *pixg,
+                   l_float32  fract)
+{
+l_int32  ws, hs, d, spp;
+PIX     *pixd, *pix32, *pixg2, *pixgr;
+
+    if (!pixs)
+        return (PIX *)ERROR_PTR("pixs not defined", __func__, NULL);
+    pixGetDimensions(pixs, &ws, &hs, &d);
+    if (d != 32 && pixGetColormap(pixs) == NULL)
+        return (PIX *)ERROR_PTR("pixs not cmapped or 32 bpp", __func__, NULL);
+    if (pixg && pixGetDepth(pixg) != 8) {
+        L_WARNING("pixg not 8 bpp; using 'fract' transparent alpha\n",
+                  __func__);
+        pixg = NULL;
+    }
+    if (!pixg && (fract < 0.0 || fract > 1.0)) {
+        L_WARNING("invalid fract; using fully opaque\n", __func__);
+        fract = 1.0;
+    }
+    if (!pixg && fract == 0.0)
+        L_WARNING("transparent alpha; image will not be blended\n", __func__);
+
+        /* Make sure input to rotation is 32 bpp rgb, and rotate it */
+    if (d != 32)
+        pix32 = pixConvertTo32(pixs);
+    else
+        pix32 = pixClone(pixs);
+    spp = pixGetSpp(pix32);
+    pixSetSpp(pix32, 3);  /* ignore the alpha channel for the rotation */
+    pixd = pixRotate(pix32, angle, L_ROTATE_AREA_MAP, L_BRING_IN_WHITE, ws, hs);
+    pixSetSpp(pix32, spp);  /* restore initial value in case it's a clone */
+    pixDestroy(&pix32);
+
+        /* Set up alpha layer with a fading border and rotate it */
+    if (!pixg) {
+        pixg2 = pixCreate(ws, hs, 8);
+        if (fract == 1.0)
+            pixSetAll(pixg2);
+        else if (fract > 0.0)
+            pixSetAllArbitrary(pixg2, (l_int32)(255.0 * fract));
+    } else {
+        pixg2 = pixResizeToMatch(pixg, NULL, ws, hs);
+    }
+    if (ws > 10 && hs > 10) {  /* see note 8 */
+        pixSetBorderRingVal(pixg2, 1,
+                            (l_int32)(255.0 * fract * AlphaMaskBorderVals[0]));
+        pixSetBorderRingVal(pixg2, 2,
+                            (l_int32)(255.0 * fract * AlphaMaskBorderVals[1]));
+    }
+    pixgr = pixRotate(pixg2, angle, L_ROTATE_AREA_MAP,
+                      L_BRING_IN_BLACK, ws, hs);
+
+        /* Combine into a 4 spp result */
+    pixSetRGBComponent(pixd, pixgr, L_ALPHA_CHANNEL);
+
+    pixDestroy(&pixg2);
+    pixDestroy(&pixgr);
+    return pixd;
+}