Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 24 additions & 3 deletions clipart/areas.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,25 @@

import logging

from PIL import Image
from reportlab.lib.utils import ImageReader

from .catalog import getClipConfig, loadClipart
from conversionState import ConversionState
from renderContext import RenderContext


def _hasEnabledShadow(decoration):
"""Return whether ``decoration`` contains a CEWE-enabled shadow."""
return (decoration is not None
and any(shadow.get('shadowEnabled') in (None, '1')
for shadow in decoration.findall('shadow')))


def processAreaClipartTag(clipartElement, areaHeight, areaRot, areaWidth, pdf, transx, transy,
clipArtDecoration, context: RenderContext,
borderProcessor):
"""Render one clipart area and its optional border."""
state: ConversionState, shadowProcessor, borderProcessor):
"""Render one clipart area and its optional decorations."""
clipartID = int(clipartElement.get('designElementId'))
if clipartID == 0:
return
Expand All @@ -29,11 +38,13 @@ def processAreaClipartTag(clipartElement, areaHeight, areaRot, areaWidth, pdf, t

colorReplacements, flipX, flipY = getClipConfig(clipartElement)
insertClipartFile(fileName, colorReplacements, transx, areaWidth, areaHeight, alpha, pdf,
transy, areaRot, flipX, flipY, clipArtDecoration, context, borderProcessor)
transy, areaRot, flipX, flipY, clipArtDecoration, context, state,
shadowProcessor, borderProcessor)


def insertClipartFile(fileName, colorReplacements, transx, areaWidth, areaHeight, alpha, pdf,
transy, areaRot, flipX, flipY, decoration, context: RenderContext,
state: ConversionState | None = None, shadowProcessor=None,
borderProcessor=None):
"""Rasterise a clipart file and draw it at the supplied area geometry."""
newWidth = int(0.5 + areaWidth * context.image_resolution / 254.0)
Expand All @@ -48,9 +59,19 @@ def insertClipartFile(fileName, colorReplacements, transx, areaWidth, areaHeight
clipart.replaceColors(colorReplacements)

clipart.convertToPngInBuffer(newWidth, newHeight, alpha, flipX, flipY)
clipartImage = None
if _hasEnabledShadow(decoration) and shadowProcessor is not None and state is not None:
# Use the rasterised SVG's alpha silhouette. In particular, open
# artwork (for example an arrow) must not cast a rectangular shadow.
clipartImage = Image.open(clipart.pngMemFile).convert('RGBA')
clipartImage.load()
clipart.pngMemFile.seek(0)
logging.debug(f"Clipart file: {fileName}")
pdf.translate(transx, transy)
pdf.rotate(-areaRot)
if clipartImage is not None:
shadowProcessor(decoration, areaHeight, areaWidth, pdf, context, state,
clipartImage, areaWidth, areaHeight, areaRotation=areaRot)
pdf.drawImage(ImageReader(clipart.pngMemFile),
context.mcf_to_reportlab * -0.5 * areaWidth,
context.mcf_to_reportlab * -0.5 * areaHeight,
Expand Down
27 changes: 16 additions & 11 deletions decorations/shadows.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,11 @@ def drawBlurredImageShadow(pdf, im, imgCropWidth_mcfunit,
# was downsampled earlier in processAreaImageTag.
pixelsPerMcfunit = im.width / imgCropWidth_mcfunit

# CEWE's displayed shadow expands by about three tenths of the nominal
# width on each side. This was calibrated against the width-only test page
# (0 .. 10 mm); the old ReportLab Table is noticeably larger at high values.
spreadRadius_px = int(round(
# CEWE's displayed shadow protrudes by about three tenths of the nominal
# width on each side. It enlarges the shadow's bounding box rather than
# thickening every opaque pixel. The distinction is invisible for a
# solid rectangle but essential for open artwork such as arrows.
spreadOutset_px = int(round(
shadowWidth_mcfunit * pixelsPerMcfunit * 0.30
))

Expand All @@ -68,18 +69,22 @@ def drawBlurredImageShadow(pdf, im, imgCropWidth_mcfunit,
# Keep the fractional radius: rounding it would make several of CEWE's
# small blur settings render identically at the configured image DPI.
blurRadius_px = shadowBlur_mcfunit * pixelsPerMcfunit * 0.5
padding_px = spreadRadius_px + int(np.ceil(3 * blurRadius_px))
blurPadding_px = int(np.ceil(3 * blurRadius_px))
padding_px = spreadOutset_px + blurPadding_px

alpha = im.getchannel('A')
shadowAlpha = Image.new(
'L', (im.width + 2 * padding_px, im.height + 2 * padding_px), 0
)
shadowAlpha.paste(alpha, (padding_px, padding_px))

if spreadRadius_px > 0:
shadowAlpha = shadowAlpha.filter(
ImageFilter.MaxFilter(2 * spreadRadius_px + 1)
)
if spreadOutset_px > 0:
# Put a scaled copy of the alpha silhouette into the enlarged shadow
# bounds. A morphological max filter would make an open SVG's thin
# strokes dramatically fatter, unlike CEWE's ``Utstikk`` control.
alpha = alpha.resize(
(im.width + 2 * spreadOutset_px,
im.height + 2 * spreadOutset_px),
Image.Resampling.LANCZOS)
shadowAlpha.paste(alpha, (blurPadding_px, blurPadding_px))

if blurRadius_px > 0:
shadowAlpha = shadowAlpha.filter(ImageFilter.GaussianBlur(blurRadius_px))
Expand Down
3 changes: 2 additions & 1 deletion pageElements.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,8 @@ def processElements(additional_fonts, fotobook, imagedir,
for clipartElement in area.findall('clipart'):
processAreaClipartTag(
clipartElement, areaHeight, areaRot, areaWidth, pdf,
transCx, transCy, decoration, context,
transCx, transCy, decoration, context, state,
processDecorationShadow,
lambda decoration, height, width, canvas:
processDecorationBorders(decoration, height, width,
canvas, context))
Binary file not shown.
Binary file not shown.
Loading
Loading