Some image protocol improvements
This commit is contained in:
parent
0bcc8bc656
commit
e19fe276c4
|
@ -4,7 +4,8 @@
|
||||||
//> using publish.organization de.qwertyuiop
|
//> using publish.organization de.qwertyuiop
|
||||||
//> using publish.name copret
|
//> using publish.name copret
|
||||||
//> using publish.version 0.0.2
|
//> using publish.version 0.0.2
|
||||||
//> using dep org.typelevel::cats-core:2.10.0
|
//> using dep org.typelevel::cats-core:2.12.0
|
||||||
//> using dep org.jline:jline:3.26.1
|
//> using dep org.jline:jline:3.28.0
|
||||||
//> using dep com.lihaoyi::os-lib:0.10.1
|
//> using dep com.lihaoyi::os-lib:0.11.3
|
||||||
//> using dep com.lihaoyi::fansi:0.5.0
|
//> using dep com.lihaoyi::fansi:0.5.0
|
||||||
|
//> using test.dep com.lihaoyi::utest:0.8.4
|
||||||
|
|
|
@ -96,7 +96,8 @@ case class Presentation(
|
||||||
case incMd @ IncludeMarkdown(_) => println(incMd.markdownBlock())
|
case incMd @ IncludeMarkdown(_) => println(incMd.markdownBlock())
|
||||||
case Image(file, None) => println(KittyGraphicsProtocol.showImage(file))
|
case Image(file, None) => println(KittyGraphicsProtocol.showImage(file))
|
||||||
case Image(file, Some(ImageSize(w, h, aspect))) =>
|
case Image(file, Some(ImageSize(w, h, aspect))) =>
|
||||||
println(KittyGraphicsProtocol.showImage(file)) // TODO
|
import KittyGraphicsProtocol.Sizing.*
|
||||||
|
println(KittyGraphicsProtocol.showImage(file, Absolute(w.toInt), Absolute(h.toInt), aspect)) // TODO
|
||||||
case cmd: TypedCommand[_] => cmd.show()
|
case cmd: TypedCommand[_] => cmd.show()
|
||||||
case Silent(actions) => actions()
|
case Silent(actions) => actions()
|
||||||
case Group(slides) => slides.foreach(executeSlide(pos))
|
case Group(slides) => slides.foreach(executeSlide(pos))
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package de.qwertyuiop.copret
|
package de.qwertyuiop.copret
|
||||||
|
|
||||||
import Terminal.*
|
import Terminal.*
|
||||||
|
import scala.util.Try
|
||||||
|
import javax.imageio.ImageIO
|
||||||
|
|
||||||
object KittyGraphicsProtocol:
|
object KittyGraphicsProtocol:
|
||||||
val MaxID = 4294967295L // max 32-bit unsigned
|
val MaxID = 4294967295L // max 32-bit unsigned
|
||||||
|
@ -9,24 +11,104 @@ object KittyGraphicsProtocol:
|
||||||
queryTerm(s"${apc}Gi=${KittyGraphicsProtocol.MaxID},s=1,v=1,a=q,t=d,f=24;AAAA${st}${csi}c")
|
queryTerm(s"${apc}Gi=${KittyGraphicsProtocol.MaxID},s=1,v=1,a=q,t=d,f=24;AAAA${st}${csi}c")
|
||||||
.contains(s"${apc}Gi=${KittyGraphicsProtocol.MaxID}")
|
.contains(s"${apc}Gi=${KittyGraphicsProtocol.MaxID}")
|
||||||
|
|
||||||
def showImage(img: os.Path) =
|
def imageSize(img: os.Path) =
|
||||||
|
Try(ImageIO.read(img.toIO)).toOption.map: image =>
|
||||||
|
SizePx(image.getWidth, image.getHeight)
|
||||||
|
|
||||||
|
enum Sizing:
|
||||||
|
case Absolute(pixels: Int)
|
||||||
|
case Relative(ratio: Double)
|
||||||
|
case Infer
|
||||||
|
|
||||||
|
case class SizePx(width: Int, height: Int)
|
||||||
|
case class SizeCells(width: Int, height: Int)
|
||||||
|
|
||||||
|
case class ImageSize(px: SizePx, cells: SizeCells)
|
||||||
|
|
||||||
|
def fitCellsToScreen(sizeCells: SizeCells, termSize: TermCells): SizeCells =
|
||||||
|
val widthRatio = sizeCells.width.toDouble / termSize.cols
|
||||||
|
val heightRatio = sizeCells.height.toDouble / termSize.rows
|
||||||
|
if widthRatio > 1 || heightRatio > 1 then
|
||||||
|
val ratio = widthRatio max heightRatio
|
||||||
|
SizeCells((sizeCells.width / ratio).toInt, (sizeCells.height / ratio).toInt)
|
||||||
|
else sizeCells
|
||||||
|
|
||||||
|
def calculateSize(
|
||||||
|
size: SizePx,
|
||||||
|
requestedWidth: Sizing,
|
||||||
|
requestedHeight: Sizing,
|
||||||
|
keepAspect: Boolean,
|
||||||
|
termSize: TermSize,
|
||||||
|
): ImageSize =
|
||||||
|
import Sizing.*
|
||||||
|
|
||||||
|
def scale(dimensionPx: Int, requestedPx: Sizing): Int =
|
||||||
|
requestedPx match
|
||||||
|
case Absolute(pixels) => pixels
|
||||||
|
case Relative(ratio) => (ratio * dimensionPx).toInt
|
||||||
|
case Infer => dimensionPx
|
||||||
|
|
||||||
|
val cell = SizePx(termSize.pixels.width / termSize.cells.cols, termSize.pixels.height / termSize.cells.rows)
|
||||||
|
|
||||||
|
val scaledWidth = scale(size.width, requestedWidth)
|
||||||
|
val scaledHeight = scale(size.height, requestedHeight)
|
||||||
|
|
||||||
|
val pixels =
|
||||||
|
if !keepAspect then SizePx(scaledWidth, scaledHeight)
|
||||||
|
else
|
||||||
|
val widthRatio = scaledWidth.toDouble / size.width
|
||||||
|
val heightRatio = scaledHeight.toDouble / size.height
|
||||||
|
if widthRatio < heightRatio then SizePx(scaledWidth, (size.height * widthRatio).toInt)
|
||||||
|
else SizePx((size.width * heightRatio).toInt, scaledHeight)
|
||||||
|
|
||||||
|
val sizeCells = SizeCells(pixels.width / cell.width, pixels.height / cell.height)
|
||||||
|
|
||||||
|
ImageSize(pixels, sizeCells)
|
||||||
|
end calculateSize
|
||||||
|
|
||||||
|
def showImage(img: os.Path): String = showImage(img, Sizing.Infer, Sizing.Infer, true)
|
||||||
|
def showImage(
|
||||||
|
img: os.Path,
|
||||||
|
requestedWidth: Sizing,
|
||||||
|
requestedHeight: Sizing,
|
||||||
|
keepAspect: Boolean,
|
||||||
|
fitToScreen: Boolean = true,
|
||||||
|
): String =
|
||||||
import java.util.Base64
|
import java.util.Base64
|
||||||
(
|
(
|
||||||
for
|
for
|
||||||
_ <- Option.when(checkSupport())(true)
|
_ <- Option.when(checkSupport())(true)
|
||||||
termSize <- Terminal.getSize()
|
termSize <- Terminal.getSize()
|
||||||
cursorPos <- Terminal.getCursorPos()
|
cursorPos <- Terminal.getCursorPos()
|
||||||
|
sizeOrig <- imageSize(img)
|
||||||
|
|
||||||
|
ImageSize(pixels, cells) = calculateSize(sizeOrig, requestedWidth, requestedHeight, keepAspect, termSize)
|
||||||
yield
|
yield
|
||||||
|
val size =
|
||||||
|
ImageSize(
|
||||||
|
pixels,
|
||||||
|
if fitToScreen then fitCellsToScreen(cells, termSize.cells)
|
||||||
|
else cells,
|
||||||
|
)
|
||||||
|
|
||||||
val image = Base64.getEncoder.encodeToString(os.read.bytes(img))
|
val image = Base64.getEncoder.encodeToString(os.read.bytes(img))
|
||||||
|
val cell = SizePx(termSize.pixels.width / termSize.cells.cols, termSize.pixels.height / termSize.cells.rows)
|
||||||
|
|
||||||
|
val commonParams = s"s=${sizeOrig.width},v=${sizeOrig.height},c=${size.cells.width},r=${size.cells.height}"
|
||||||
|
logger.info(s"Image size (px): ${sizeOrig.width} x ${sizeOrig.height}")
|
||||||
|
logger.info(s"Cellsize (px): ${cell.width} x ${cell.height}")
|
||||||
|
logger.info(s"Preferred size (px) ${size.px}")
|
||||||
|
logger.info(s"Preferred size (Cells) ${size.cells}")
|
||||||
|
|
||||||
if image.length > 4096 then
|
if image.length > 4096 then
|
||||||
val chunks = image.grouped(4096).toVector
|
val chunks = image.grouped(4096).toVector
|
||||||
val width = termSize.cols - cursorPos.cols * 2
|
|
||||||
|
|
||||||
s"${apc}Gf=100,t=d,m=1,a=T,c=${width};${chunks.head}${st}" +
|
s"${apc}Gf=100,t=d,m=1,a=T,${commonParams};${chunks.head}${st}" +
|
||||||
chunks.tail.init.map(c => s"${apc}Gm=1;${c}${st}").mkString +
|
chunks.tail.init.map(c => s"${apc}Gm=1;${c}${st}").mkString +
|
||||||
s"${apc}Gm=0;${chunks.last}${st}"
|
s"${apc}Gm=0;${chunks.last}${st}"
|
||||||
else s"${apc}Gf=100,t=d,a=T;${image}${st}"
|
else s"${apc}Gf=100,t=d,a=T,${commonParams};${image}${st}"
|
||||||
).getOrElse("Could not show image")
|
).getOrElse("Could not show image")
|
||||||
|
end showImage
|
||||||
|
|
||||||
trait Param:
|
trait Param:
|
||||||
def code: String
|
def code: String
|
||||||
|
|
|
@ -37,13 +37,10 @@ object Terminal:
|
||||||
def queryTerm(query: String): String =
|
def queryTerm(query: String): String =
|
||||||
val ttyIn = new java.io.FileInputStream("/dev/tty")
|
val ttyIn = new java.io.FileInputStream("/dev/tty")
|
||||||
val ttyOut = new java.io.PrintStream(new java.io.FileOutputStream("/dev/tty"))
|
val ttyOut = new java.io.PrintStream(new java.io.FileOutputStream("/dev/tty"))
|
||||||
logger.info(s"Querying terminal: ${query.replaceAllLiterally("\u001b", "<ESC>")}")
|
|
||||||
ttyOut.print(query)
|
ttyOut.print(query)
|
||||||
var response = scala.collection.mutable.ArrayBuffer[Int]()
|
var response = scala.collection.mutable.ArrayBuffer[Int]()
|
||||||
response += ttyIn.read()
|
response += ttyIn.read()
|
||||||
logger.info(s"Response: ${response}")
|
|
||||||
while (ttyIn.available > 0 && response.last != '\u0007')
|
while (ttyIn.available > 0 && response.last != '\u0007')
|
||||||
logger.info(s"Response(cont): ${response}")
|
|
||||||
response += ttyIn.read()
|
response += ttyIn.read()
|
||||||
new String(response.toArray, 0, response.length)
|
new String(response.toArray, 0, response.length)
|
||||||
|
|
||||||
|
@ -104,20 +101,27 @@ object Terminal:
|
||||||
case CursorPosResponse(rows, cols) => Some(CursorPos(cols.toInt, rows.toInt))
|
case CursorPosResponse(rows, cols) => Some(CursorPos(cols.toInt, rows.toInt))
|
||||||
case _ => None
|
case _ => None
|
||||||
|
|
||||||
def getSize(): Option[CellsSize] =
|
def getSizeCells(): Option[TermCells] =
|
||||||
queryTerm(s"${csi}s${csi}999;999H${csi}6n${csi}u") match
|
queryTerm(s"${csi}s${csi}999;999H${csi}6n${csi}u") match
|
||||||
case CursorPosResponse(rows, cols) => Some(CellsSize(cols.toInt, rows.toInt))
|
case CursorPosResponse(rows, cols) => Some(TermCells(cols.toInt, rows.toInt))
|
||||||
case _ => None
|
case _ => None
|
||||||
|
|
||||||
private val SizeResponse = """\u001b\[4;(\d+);(\d+)t""".r
|
private val SizeResponse = """\u001b\[4;(\d+);(\d+)t""".r
|
||||||
|
|
||||||
case class PixelSize(width: Int, height: Int)
|
case class TermPixels(width: Int, height: Int)
|
||||||
case class CellsSize(cols: Int, rows: Int)
|
case class TermCells(cols: Int, rows: Int)
|
||||||
|
case class TermSize(pixels: TermPixels, cells: TermCells)
|
||||||
case class CursorPos(cols: Int, rows: Int)
|
case class CursorPos(cols: Int, rows: Int)
|
||||||
def getSizePixels: Option[PixelSize] = queryTerm(s"${csi}14t") match
|
def getSizePixels(): Option[TermPixels] = queryTerm(s"${csi}14t") match
|
||||||
case SizeResponse(rows, cols) => Some(PixelSize(cols.toInt, rows.toInt))
|
case SizeResponse(rows, cols) => Some(TermPixels(cols.toInt, rows.toInt))
|
||||||
case _ => None
|
case _ => None
|
||||||
|
|
||||||
|
def getSize(): Option[TermSize] =
|
||||||
|
for
|
||||||
|
pixels <- getSizePixels()
|
||||||
|
cells <- getSizeCells()
|
||||||
|
yield TermSize(pixels, cells)
|
||||||
|
|
||||||
end Terminal
|
end Terminal
|
||||||
|
|
||||||
private[copret] trait TerminalSyntax:
|
private[copret] trait TerminalSyntax:
|
||||||
|
|
Loading…
Reference in a new issue