############################################################################
#
#	File:     imxform.icn
#
#	Subject:  Procedures to transform image matrices
#
#	Author:   Ralph E. Griswold
#
#	Date:     June 10, 2001
#
############################################################################
#
#   This file is in the public domain.
#
############################################################################
#
#  This file contains procedures that manipulate matrices that represent
#  images.
#
############################################################################
#
#  Requires:  Version 8.11, graphics
#
############################################################################
#
#  Links:  factors, imsutils, random, strings
#
############################################################################

link factors
link imsutils
link random
link strings

#
#  Reduces a image matrix to the smallest equivalent one.

procedure imxreduce(rows)

   rows := imxcollap(rows)
   rows := imxrotate(rows, 90)
   rows := imxcollap(rows)
   rows := imxrotate(rows, -90)

   return rows

end

procedure imxcollap(rows)
   local size, fact

   size := *rows
   every fact := !pfactors(size) do {
      while rowdupl(rows, fact) do {
         size /:= fact
         rows := rows[1+:size]
         }
      }

   return rows

end

procedure rowdupl(rows, n)
   local span, i, j

   if *rows % n ~= 0 then fail

   span := *rows / n

   every i := 1 to n - 1 do
      every j := 1 to span do
         if rows[j] ~== rows[i * span + j] then fail

   return

end

#
#  Produces the inclusive "or" of two image matrices.

procedure imxor(rows1, rows2)
   local i, j

   if (*rows1 ~= *rows2) | (**rows1 ~= **rows2) then fail

   rows1 := copy(rows1)

   every i := 1 to *rows1 do
      every j := upto('1', rows2[i]) do
         rows1[i][j] := "1"

   return rows1

end

#
#  Produces the "and" of two image matrices.

procedure imxand(rows1, rows2)
   local i, j

   if (*rows1 ~= *rows2) | (**rows1 ~= **rows2) then fail

   rows1 := copy(rows1)

   every i := 1 to *rows1 do
      every j := upto('0', rows2[i]) do
         rows1[i][j] := "0"

   return rows1

end

#
#  Produces the exclusive "or" of two image matrices.

procedure imxxor(rows1, rows2)
   local i, j

   if (*rows1 ~= *rows2) | (**rows1 ~= **rows2) then fail

   rows1 := copy(rows1)

   every i := 1 to *rows1 do
      every j := 1 to **rows1 do
         rows1[i][j] := if rows1[i][j] == rows2[i][j] then "0" else "1"

   return rows1

end

#
#  Scrambles a image matrix by shuffling it.  If dir is "h", the columns of each row
#  are scrambled; if "v", the the rows are scrambled.  If "b", bits are
#  scrambled throughout the image matrix.

procedure imxscramb(rows, dir)
   local i, all

   case dir of {
      "h":  {
          every i := 1 to *rows do
             rows[i] := shuffle(rows[i])
          }
      "v":  rows := shuffle(rows)
      "b" | &null:  {
         all := ""
         every all ||:= !rows
         all := shuffle(all)
         every i := 1 to *rows do {
            rows[i] := left(all, *rows[1])
            all[1 +: *rows[1]] := ""
            }
         }
      default:   stop("*** illegal specification in scramble()")
      }

   return rows

end 

#
#  Create bit-shifted copy of an image matrix.  If dir is "h", then the
#  shift is horizontal; if "v", vertical. The default is horizontal.
#  Positive shift is to the right for horizontal shifts, downward for vertical
#  shifts.  The default shift is 0 and the default direction is horizontal.

procedure imxshift(rows, shift, dir)
   local i

   /shift := 0

   rows := copy(rows)

   case dir of {
      "h" | &null: {			# horizontal shift
         every i := 1 to *rows do
            rows[i] := rotate(rows[i], -shift)
         }
      "v": {				# vertical shift
         if shift > 0 then
            every 1 to shift do
               push(rows, pull(rows))
         else if shift < 0 then
            every 1 to -shift do
               put(rows, pop(rows))
         }
      default:  stop("*** illegal specification in imxshift()")
      }

   return rows

end

#
#  Place a border around a image matrix.  l, r, t, and b specify the number of bits
#  to add at the left, right, top, and bottom, respectively.  c specifies
#  the color of the border, "0" for white, "1" for black.

procedure imxborder(rows, l, r, t, b, c)
   local i, row, left, right

   /l := 1
   /r := 1
   /t := 1
   /b := 1
   /c := "0"

   if l = r = t = b = 0 then return rows

   row := repl(c, *rows[1] + l + r)
   left := repl(c, l)
   right := repl(c, r)

   every i := 1 to *rows do
       rows[i] := left || rows[i] || right

   every 1 to t do
      push(rows, row)

   every 1 to b do
      put(rows, row)

   return rows

end

#
#  Crop a image matrix.  l, r, t, and b specify the number of bits
#  to crop at the left, right, top, and bottom, respectively.

procedure imxcrop(rows, l, r, t, b)
   local i

   /l := 0
   /r := 0
   /t := 0
   /b := 0

   if l = r = t = b = 0 then return rows

   if ((*rows[1] - l - r) | (*rows - t - b)) < 4 then fail

   every 1 to t do
      get(rows)

   every 1 to b do
      pull(rows)

   every i := 1 to *rows do
       rows[i] := rows[i][l + 1 : -r]

   return rows

end

#  Creates a tile in every other pixel is discarded.  dir determines the
#  direction is which the halving is done.  If dir is "b" or null, it's
#  done both vertically and horizontally.  If dir is "v", it's only done
#  vertically, while if dir is "v", it's done only vertically.
#  If choice is "o" or null, odd-numbered rows or columns are kept;
#  if "e", the even-numbered ones.

procedure imxhalve(rows, dir, choice)
   local newrows, i

   choice := if choice === ("o" | &null) then 1 else 0
   newrows := []

   case dir of {
      "v": {
         every i := choice to *rows by 2 do
            put(newrows, rows[i])
         }
      "h":  every put(newrows, decollate(!rows, choice))
      "b" | &null: return imxhalve(imxhalve(rows, "v", choice), "h", choice)
      }

   return newrows

end
         
#
#  Creates a tile in which each pixel doubled.  dir determines the
#  direction in which the doubling is done.  If dir is "b" or null, it's
#  done both horizontally and vertically.  If dir is "v", it's only done
#  vertically, while if dir is "h", it's done only horizontally.

procedure imxdouble(rows, dir)
   local row, newrows

   newrows := []

   case dir of {
      "v":  {
         every row := !rows do
            put(newrows, row, row)
         }
      "h":  {
         every row := !rows do
            put(newrows, collate(row, row))
         }
      "b" | &null: return imxdouble(imxdouble(rows, "v"), "h")
      }

   return newrows

end

#
#  Flip image matrix.  The possible values of dir are "h" (horizontal flip),
#  "v" (vertical flip), "l" (left diagonal), and "r" (right diagonal).
#  (The left diagonal extends from the upper left corner to the bottom
#  right corner; the right diagonal from the upper right to the lower
#  left.

procedure imxflip(rows, dir)
   local newrows, x, y, i

   case dir of {
      "l": {
         newrows := imxrotate(rows)
         every y := 1 to *rows do
            every x := 1 to *rows[1] do
               newrows[x, y] := rows[y, x]
         }
      "r": {
         newrows := list(*rows[1], repl("0", *rows))
         every y := 1 to *rows do
            every x := 1 to *rows[1] do
               if rows[y, x] == "1" then
                  newrows[x, y] := "1"
         }
      "h":  {
         newrows := copy(rows)
         every i := 1 to *rows do
            newrows[i] := reverse(newrows[i])
         }
      "v":  {
         newrows := copy(rows)
         every i := 1 to *rows / 2 do
           newrows[i] :=: newrows[-i]
         }
      default:  stop("*** illegal flip specification in imxflip()")
      }

   return newrows
      
end

#
#  Invert white and black bits in image matrix specification

procedure imxinvert(rows)
   local i

   every i := 1 to *rows do
      rows[i] := map(rows[i], "10", "01")

   return rows

end

#
#  Reduce image matrix to its smallest equivalent form (with at least 4 columns).
#  Limited to square image matrices for portability -- other possibilities exist
#  for operating on and/or producing image matrices that are not square.


procedure imxminim(rows)
   local halfw, halfh, i

   if (*rows ~= *rows[1]) | (*rows % 2 ~= 0) then return rows

   repeat {
  
      if *rows[1] < 8 then break		# can't reduce to < 4 columns

      halfw := *rows[1] / 2
      halfh := *rows / 2
   
      every i := 1 to halfh do			# check rows in top and bottom
         if (rows[i] ~== rows[i + halfh]) |
            (rows[i][1+:halfw] ~== rows[i][0-:halfw]) then break break
   
      every 1 to halfh do			# reducible; remove rows
         pop(rows)
   
      every i := 1 to halfh do			# truncate rows
         rows[i] := rows[i][1+:halfw]
   
      }

   return rows

end

#  Create rotated copy of an image matrix.  If dir is "cw" or "90", rotation is
#  90 degrees clockwise; if "ccw" or "-90", 90 degrees counter-clockwise.
#  If dir is "180", rotation is 180 degrees.  The default is "cw".

procedure imxrotate(rows, dir)
   local newrows, i, row, pix

   /dir := "cw"

   case string(dir) of {
      "ccw" | "-90":  {			# counter-clockwise
         newrows := list(*rows[1], "")
         every row := !rows do  {
            i := 0
            every pix := !row do
               newrows[i -:= 1] ||:= pix
            }
        }
      "cw" | "90" | &null:  {			# clockwise
         newrows := list(*rows[1], "")
         every row := !rows do {
            i := 0
            every pix := !row do
               newrows[i +:= 1] := pix || newrows[i]
            }
         }
      "180": {
         newrows := []
         every push(newrows, reverse(!rows))
         }
      default:  stop("*** illegal rotation specification in imxrotate()")
   }

   return newrows

end

#
#  Trim border whitespace from image matrix

procedure imxtrim(rows)

   while (*rows > 4) & not(upto('1', rows[1])) do
      get(rows)

   while (*rows > 4) & not(upto('1', rows[-1])) do
      pull(rows)

   rows := imxrotate(rows, "cw")

   while (*rows > 4) & not(upto('1', rows[1])) do
      get(rows)

   while (*rows > 4) & not(upto('1', rows[-1])) do
      pull(rows)
 
   return imxrotate(rows, "ccw")

end

#
#  Centers non-white portion of image matrix

procedure imxcenter(rows, w, h)
   local rw, rh, vert, horz, t, l

   rows := imxtrim(rows)

   rw := *rows[1]
   rh := *rows

   if (rh = h) & (rw = w) then return rows
   if (rh > h) | (rw > w) then fail

   horz := w - rw
   vert := h - rh
   l := horz / 2
   t := vert / 2

   return imxborder(rows, l, horz - l, t, vert - t)

end

#  Create a blank i-by-j image matrix

procedure imxcreate(i, j)

   return list(i, repl("0", j))

end
