diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,17 @@
 
+6.7
+---
+
+API changes:
+
+* `Graphics.Vty.Image`: added `rewriteAttrs` for performing arbitrary
+  attribute transformations over images
+
+Other improvements:
+
+* Improved image performance by re-using known text string widths
+  (thanks Andrzej Rybczak)
+
 6.6
 ---
 
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -150,6 +150,14 @@
 
  - Patches written completely or partially by AI are unlikely to be
    accepted. Please disclose any AI use.
+ - Use good commit hygiene: each commit should make a single,
+   independent logical change and its commit message should clearly
+   describe the motivation for that change and what it does at a high
+   level. If a commit changes something that has tests, the commit
+   should also include test changes as well, or add new tests if
+   possible. "WIP" commits, commits that mix up style and substance
+   (e.g. reformatting style while also adding a feature), etc., are to
+   be avoided.
  - If you want to take on big things, talk to me first; let's have a
    design/vision discussion before you start coding. Create a GitHub
    issue and we can use that as the place to hash things out.
diff --git a/src/Graphics/Vty/Image.hs b/src/Graphics/Vty/Image.hs
--- a/src/Graphics/Vty/Image.hs
+++ b/src/Graphics/Vty/Image.hs
@@ -42,6 +42,7 @@
   , translate
   , translateX
   , translateY
+  , rewriteAttrs
   -- * Character width functions
   , safeWcwidth
   , safeWcswidth
diff --git a/src/Graphics/Vty/Image/Internal.hs b/src/Graphics/Vty/Image/Internal.hs
--- a/src/Graphics/Vty/Image/Internal.hs
+++ b/src/Graphics/Vty/Image/Internal.hs
@@ -9,6 +9,7 @@
   , imageWidth
   , horizJoin
   , vertJoin
+  , rewriteAttrs
 
   , ppImageStructure
   , clipText
@@ -120,6 +121,26 @@
     -- Any image of zero size equals the empty image.
     | EmptyImage
     deriving (Eq, Generic, Show, Read)
+
+-- | Rewrite all attributes in the image using the specified
+-- transformation.
+rewriteAttrs :: (Attr -> Attr) -> Image -> Image
+rewriteAttrs f i =
+    case i of
+        HorizText { attr = a } ->
+            i { attr = f a }
+        HorizJoin { partLeft = l, partRight = r } ->
+            i { partLeft = rewriteAttrs f l
+              , partRight = rewriteAttrs f r
+              }
+        VertJoin { partTop = t, partBottom = b } ->
+            i { partTop = rewriteAttrs f t
+              , partBottom = rewriteAttrs f b
+              }
+        Crop { croppedImage = c } ->
+            i { croppedImage = rewriteAttrs f c }
+        BGFill {} -> i
+        EmptyImage -> i
 
 -- | pretty print just the structure of an image.
 ppImageStructure :: Image -> String
diff --git a/src/Graphics/Vty/PictureToSpans.hs b/src/Graphics/Vty/PictureToSpans.hs
--- a/src/Graphics/Vty/PictureToSpans.hs
+++ b/src/Graphics/Vty/PictureToSpans.hs
@@ -247,7 +247,7 @@
 --
 addMaybeClipped :: forall s . Image -> BlitM s ()
 addMaybeClipped EmptyImage = return ()
-addMaybeClipped (HorizText a textStr ow _cw) = do
+addMaybeClipped (HorizText a textStr ow cw) = do
     -- This assumes that text spans are only 1 row high.
     s <- use skipRows
     when (s < 1) $ do
@@ -258,7 +258,7 @@
         if leftClipped || rightClipped
             then let textStr' = clipText textStr leftClip rightClip
                  in addUnclippedText a textStr'
-            else addUnclippedText a textStr
+            else snocOp (TextSpan a ow cw textStr) =<< use rowOffset
 addMaybeClipped (VertJoin topImage bottomImage _ow oh) = do
     when (imageHeight topImage + imageHeight bottomImage > 0) $
         addMaybeClippedJoin "vert_join" skipRows remainingRows rowOffset
diff --git a/vty.cabal b/vty.cabal
--- a/vty.cabal
+++ b/vty.cabal
@@ -1,5 +1,5 @@
 name:                vty
-version:             6.6
+version:             6.7
 license:             BSD3
 license-file:        LICENSE
 author:              AUTHORS
