ivg

package module
v0.0.9 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 17, 2023 License: BSD-3-Clause Imports: 2 Imported by: 0

README

ivg

import "github.com/reactivego/ivg"

Go Reference

Package ivg provides rendering of IconVG icons using a Rasterizer interface. IconVG is a binary format for simple vector graphic icons.

The original IconVG code was changed to render an IconVG graphic using a vector graphics Rasterizer interface. The original code rendered to a bitmap image. The use of the rasterizer allows implementing different rasterizers for different purposes. For example, a rasterizer that renders to a bitmap image, or a rasterizer that renders to a gioui.org context.

The name of the iconvg package has been changed to ivg so we don't confuse people about what's what.

File Format Versions

In order for the IconVG format to support animation in future incarnations. The format was simplified and updated to version 1 (FFV1), renaming the original format to FFV0 retroactively.

FFV1 targets representing static vector graphics icons, while the future FFV2 will target representing animated vector graphics icons.

The rationale for this was dicussed in a github proposal: File Format Versions 1, 2 and Beyond

Below are links to the different File Format Versions of the spec:

NOTE: This package implements the FFV0 version of the IconVG format.

Code Organization

The original purpose of IconVG was to convert a material design icon in SVG format to a binary data blob that could be embedded in a Go program.

The code is organized in several packages that can be combined in different ways to create different IconVG render pipelines. The Destination interface is implemented both by the Encoder in package encode and by the Renderer in package render. The Generator type in the generator package just uses a Destination and doesn't care whether calls are generating a data blob or render directly to a Rasterizer via the Renderer.

// Destination handles the actions decoded from an IconVG graphic's opcodes.
//
// When passed to Decode, the first method called (if any) will be Reset. No
// methods will be called at all if an error is encountered in the encoded form
// before the metadata is fully decoded.
type Destination interface {
	Reset(viewbox ViewBox, palette [64]color.RGBA)
	CSel() uint8
	SetCSel(cSel uint8)
	NSel() uint8
	SetNSel(nSel uint8)
	SetCReg(adj uint8, incr bool, c Color)
	SetNReg(adj uint8, incr bool, f float32)
	SetLOD(lod0, lod1 float32)

	StartPath(adj uint8, x, y float32)
	ClosePathEndPath()
	ClosePathAbsMoveTo(x, y float32)
	ClosePathRelMoveTo(x, y float32)

	AbsHLineTo(x float32)
	RelHLineTo(x float32)
	AbsVLineTo(y float32)
	RelVLineTo(y float32)
	AbsLineTo(x, y float32)
	RelLineTo(x, y float32)
	AbsSmoothQuadTo(x, y float32)
	RelSmoothQuadTo(x, y float32)
	AbsQuadTo(x1, y1, x, y float32)
	RelQuadTo(x1, y1, x, y float32)
	AbsSmoothCubeTo(x2, y2, x, y float32)
	RelSmoothCubeTo(x2, y2, x, y float32)
	AbsCubeTo(x1, y1, x2, y2, x, y float32)
	RelCubeTo(x1, y1, x2, y2, x, y float32)
	AbsArcTo(rx, ry, xAxisRotation float32, largeArc, sweep bool, x, y float32)
	RelArcTo(rx, ry, xAxisRotation float32, largeArc, sweep bool, x, y float32)
}

A parser of SVG files reads the SVG and then calls methods on a Destination to produce a binary data blob.

For Material Design icons:

mdicons/Parse -> [Destination]encode/Encoder -> []byte

For more complex SVGs a Generator supports handling of e.g. gradients and transforms. The Generator is hooked up to a Destination to produce the binary data blob.

svgicon/Parse -> generate/Generator -> [Destination]encode/Encoder -> []byte

To actually render the icon, the binary data blob would be passed to a Decoder that would call methods on a Renderer hooked up to a Rasterizer to render the icon.

// Rasterizer is a 2-D vector graphics rasterizer.
type Rasterizer interface {
	// Reset resets a Rasterizer as if it was just returned by NewRasterizer.
	// This includes setting z.DrawOp to draw.Over.
	Reset(w, h int)
	// Size returns the width and height passed to NewRasterizer or Reset.
	Size() image.Point
	// Bounds returns the rectangle from (0, 0) to the width and height passed to
	// Reset.
	Bounds() image.Rectangle
	// Pen returns the location of the path-drawing pen: the last argument to the
	// most recent XxxTo call.
	Pen() (x, y float32)
	// MoveTo starts a new path and moves the pen to (ax, ay). The coordinates
	// are allowed to be out of the Rasterizer's bounds.
	MoveTo(ax, ay float32)
	// LineTo adds a line segment, from the pen to (bx, by), and moves the pen to
	// (bx, by). The coordinates are allowed to be out of the Rasterizer's
	// bounds.
	LineTo(bx, by float32)
	// QuadTo adds a quadratic Bézier segment, from the pen via (bx, by) to (cx,
	// cy), and moves the pen to (cx, cy). The coordinates are allowed to be out
	// of the Rasterizer's bounds.
	QuadTo(bx, by, cx, cy float32)
	// CubeTo adds a cubic Bézier segment, from the pen via (bx, by) and (cx, cy)
	// to (dx, dy), and moves the pen to (dx, dy). The coordinates are allowed to
	// be out of the Rasterizer's bounds.
	CubeTo(bx, by, cx, cy, dx, dy float32)
	// ClosePath closes the current path.
	ClosePath()
	// Draw aligns r.Min in z with sp in src and then replaces the rectangle r in
	// z with the result of a Porter-Duff composition. The vector paths
	// previously added via the XxxTo calls become the mask for drawing src onto
	// z.
	Draw(r image.Rectangle, src image.Image, sp image.Point)
}

Decoding a blob and rendering it to a Rasterizer:

[]byte -> decode/Decoder -> [Destination]render/Renderer -> [Rasterizer]raster/vec/Rasterizer

To render and icon from SVG, the Generator can also be hooked up to the Renderer directly and the Encoder/Decoder phase would be skipped.

svgicon/Parse -> generate/Generator -> [Destination]render/Renderer -> [Rasterizer]raster/vec/Rasterizer

Changes

This package changes the original IconVG code in several ways. The most important changes w.r.t. the original IconVG code are:

  1. Separate code into packages with a clear purpose and responsibility for better cohesion and less coupling.
  2. Split icon encoding into encode and generate package.
  3. SVG gradient and path support is now part of generate package.
  4. Rename Rasterizer to Renderer and place it in the render package.
  5. Move Destination interface into root ivg package.
  6. Make both Encoder and Renderer implement Destination.
  7. Make both Decoder and Generator use only Destination interface.
  8. Generator can now directly render by plugging in a Renderer (very useful).
  9. Encoder can be plugged directly into a Decoder (useful for testing).
  10. Abstract away rasterizing into a seperate package raster
    • Declare interface Rasterizer.
    • Declare interface GradientConfig implemented by Renderer.
  11. Create a rasterizer using "golang.org/x/image/vector" in directory raster/vec
  12. Create examples in the example folder.
    • playarrow simplest example of rendering an icon, see below.
    • actioninfo generate an icon on the fly, render it and cache the result, see below.
    • The following examples allow you to see rendering and speed differences between rasterizers by clicking on the image to switch rasterizer.
      • icons renders golang.org/x/exp/shiny/materialdesign/icons. see below.
      • favicon vector image with several blended layers. see below.
      • cowbell vector image with several blended layers including gradients. see below.
      • gradients vector image with lots of gradients. see below.

Acknowledgement

The code in this package is based on golang.org/x/exp/shiny/iconvg.

The specification of the IconVG format has recently been moved to a separate repository github.com/google/iconvg.

License

Everything under the raster folder is Unlicense OR MIT (whichever you prefer). See file raster/LICENSE.

All the other code is is governed by a BSD-style license that can be found in the LICENSE file.

Documentation

Overview

Package ivg provides rendering of IconVG icons.

IconVG (github.com/google/iconvg) is a compact, binary format for simple vector graphics: icons, logos, glyphs and emoji.

The code in this package does away with rendering the icon to an intermediate bitmap image and instead directly uses a vector Rasterizer interface.

Example
package main

import (
	"image"
	"image/draw"
	"log"
	"os"
	"path/filepath"

	"github.com/reactivego/ivg/decode"
	"github.com/reactivego/ivg/raster/vec"
	"github.com/reactivego/ivg/render"
)

func main() {
	ivgData, err := os.ReadFile(filepath.FromSlash("testdata/action-info.lores.ivg"))
	if err != nil {
		log.Fatal(err)
	}

	const width = 24
	dst := image.NewAlpha(image.Rect(0, 0, width, width))
	var z render.Renderer
	z.SetRasterizer(&vec.Rasterizer{Dst: dst, DrawOp: draw.Src}, dst.Bounds())
	if err := decode.Decode(&z, ivgData); err != nil {
		log.Fatal(err)
	}

	const asciiArt = ".++8"
	buf := make([]byte, 0, width*(width+1))
	for y := 0; y < width; y++ {
		for x := 0; x < width; x++ {
			a := dst.AlphaAt(x, y).A
			buf = append(buf, asciiArt[a>>6])
		}
		buf = append(buf, '\n')
	}
	os.Stdout.Write(buf)

}
Output:

........................
........................
........++8888++........
......+8888888888+......
.....+888888888888+.....
....+88888888888888+....
...+8888888888888888+...
...88888888..88888888...
..+88888888..88888888+..
..+888888888888888888+..
..88888888888888888888..
..888888888..888888888..
..888888888..888888888..
..888888888..888888888..
..+88888888..88888888+..
..+88888888..88888888+..
...88888888..88888888...
...+8888888888888888+...
....+88888888888888+....
.....+888888888888+.....
......+8888888888+......
........++8888++........
........................
........................

Index

Examples

Constants

View Source
const (
	MidViewBox          = 0
	MidSuggestedPalette = 1
)
View Source
const (
	// Min aligns min of ViewBox with min of rect
	Min = 0.0
	// Mid aligns mid of ViewBox with mid of rect
	Mid = 0.5
	// Max aligns max of ViewBox with max of rect
	Max = 1.0
)
View Source
const Magic = "\x89IVG"

Variables

View Source
var DefaultMetadata = Metadata{
	ViewBox: DefaultViewBox,
	Palette: DefaultPalette,
}

DefaultMetadata combines the default ViewBox and the default Palette.

View Source
var DefaultPalette = [64]color.RGBA{
	{0x00, 0x00, 0x00, 0xff},
	{0x00, 0x00, 0x00, 0xff},
	{0x00, 0x00, 0x00, 0xff},
	{0x00, 0x00, 0x00, 0xff},
	{0x00, 0x00, 0x00, 0xff},
	{0x00, 0x00, 0x00, 0xff},
	{0x00, 0x00, 0x00, 0xff},
	{0x00, 0x00, 0x00, 0xff},
	{0x00, 0x00, 0x00, 0xff},
	{0x00, 0x00, 0x00, 0xff},
	{0x00, 0x00, 0x00, 0xff},
	{0x00, 0x00, 0x00, 0xff},
	{0x00, 0x00, 0x00, 0xff},
	{0x00, 0x00, 0x00, 0xff},
	{0x00, 0x00, 0x00, 0xff},
	{0x00, 0x00, 0x00, 0xff},
	{0x00, 0x00, 0x00, 0xff},
	{0x00, 0x00, 0x00, 0xff},
	{0x00, 0x00, 0x00, 0xff},
	{0x00, 0x00, 0x00, 0xff},
	{0x00, 0x00, 0x00, 0xff},
	{0x00, 0x00, 0x00, 0xff},
	{0x00, 0x00, 0x00, 0xff},
	{0x00, 0x00, 0x00, 0xff},
	{0x00, 0x00, 0x00, 0xff},
	{0x00, 0x00, 0x00, 0xff},
	{0x00, 0x00, 0x00, 0xff},
	{0x00, 0x00, 0x00, 0xff},
	{0x00, 0x00, 0x00, 0xff},
	{0x00, 0x00, 0x00, 0xff},
	{0x00, 0x00, 0x00, 0xff},
	{0x00, 0x00, 0x00, 0xff},
	{0x00, 0x00, 0x00, 0xff},
	{0x00, 0x00, 0x00, 0xff},
	{0x00, 0x00, 0x00, 0xff},
	{0x00, 0x00, 0x00, 0xff},
	{0x00, 0x00, 0x00, 0xff},
	{0x00, 0x00, 0x00, 0xff},
	{0x00, 0x00, 0x00, 0xff},
	{0x00, 0x00, 0x00, 0xff},
	{0x00, 0x00, 0x00, 0xff},
	{0x00, 0x00, 0x00, 0xff},
	{0x00, 0x00, 0x00, 0xff},
	{0x00, 0x00, 0x00, 0xff},
	{0x00, 0x00, 0x00, 0xff},
	{0x00, 0x00, 0x00, 0xff},
	{0x00, 0x00, 0x00, 0xff},
	{0x00, 0x00, 0x00, 0xff},
	{0x00, 0x00, 0x00, 0xff},
	{0x00, 0x00, 0x00, 0xff},
	{0x00, 0x00, 0x00, 0xff},
	{0x00, 0x00, 0x00, 0xff},
	{0x00, 0x00, 0x00, 0xff},
	{0x00, 0x00, 0x00, 0xff},
	{0x00, 0x00, 0x00, 0xff},
	{0x00, 0x00, 0x00, 0xff},
	{0x00, 0x00, 0x00, 0xff},
	{0x00, 0x00, 0x00, 0xff},
	{0x00, 0x00, 0x00, 0xff},
	{0x00, 0x00, 0x00, 0xff},
	{0x00, 0x00, 0x00, 0xff},
	{0x00, 0x00, 0x00, 0xff},
	{0x00, 0x00, 0x00, 0xff},
	{0x00, 0x00, 0x00, 0xff},
}

DefaultPalette is the default Palette. Its values should not be modified.

View Source
var DefaultViewBox = ViewBox{
	MinX: -32, MinY: -32,
	MaxX: +32, MaxY: +32,
}

DefaultViewBox is the default ViewBox. Its values should not be modified.

View Source
var MagicBytes = []byte(Magic)

Functions

func DecodeGradient

func DecodeGradient(c color.RGBA) (cBase, nBase, shape, spread, nStops uint8)

DecodeGradient returns the gradient parameters from a non-sensical RGBA color encoding a gradient.

func EncodeGradient

func EncodeGradient(cBase, nBase, shape, spread, nStops uint8) color.RGBA

EncodeGradient returns a non-sensical RGBA color encoding gradient parameters.

func Is1

func Is1(c color.RGBA) bool

func Is2

func Is2(c color.RGBA) bool

func Is3

func Is3(c color.RGBA) bool

func ValidAlphaPremulColor

func ValidAlphaPremulColor(c color.RGBA) bool

func ValidGradient

func ValidGradient(c color.RGBA) bool

ValidGradient returns true if the RGBA color is non-sensical

Types

type Color

type Color struct {
	// contains filtered or unexported fields
}

Color is an IconVG color, whose RGBA values can depend on context. Some Colors are direct RGBA values. Other Colors are indirect, referring to an index of the custom palette, a color register of the decoder virtual machine, or a blend of two other Colors.

See the "Colors" section in the package documentation for details.

func BlendColor

func BlendColor(t, c0, c1 uint8) Color

BlendColor returns an indirect Color that blends two other Colors. Those two other Colors must both be encodable as a 1 byte color.

To blend a Color that is not encodable as a 1 byte color, first load that Color into a CREG color register, then call CRegColor to produce a Color that is encodable as a 1 byte color. See testdata/favicon.ivg for an example.

See the "Colors" section in the package documentation for details.

func CRegColor

func CRegColor(i uint8) Color

CRegColor returns an indirect Color referring to a color register of the decoder virtual machine.

func DecodeColor1

func DecodeColor1(x byte) Color

func PaletteIndexColor

func PaletteIndexColor(i uint8) Color

PaletteIndexColor returns an indirect Color referring to an index of the custom palette.

func RGBAColor

func RGBAColor(c color.RGBA) Color

RGBAColor returns a direct Color.

func (Color) Encode1

func (c Color) Encode1() (x byte, ok bool)

func (Color) Encode2

func (c Color) Encode2() (x [2]byte, ok bool)

func (Color) Encode3Direct

func (c Color) Encode3Direct() (x [3]byte, ok bool)

func (Color) Encode3Indirect

func (c Color) Encode3Indirect() (x [3]byte, ok bool)

func (Color) Encode4

func (c Color) Encode4() (x [4]byte, ok bool)

func (Color) Is1

func (c Color) Is1() bool

func (Color) Is2

func (c Color) Is2() bool

func (Color) Is3

func (c Color) Is3() bool

func (Color) RGBA

func (c Color) RGBA() (color.RGBA, bool)

RGBA returns the color as a color.RGBA when that is its color type and the color is a valid premultiplied color. If the color is of a different color type or invalid, it will return a opaque black and false.

func (Color) Resolve

func (c Color) Resolve(palette *[64]color.RGBA, cReg *[64]color.RGBA) color.RGBA

Resolve resolves the Color's RGBA value, given its context: the custom palette and the color registers of the decoder virtual machine.

func (Color) String

func (c Color) String() string

type ColorType

type ColorType uint8

ColorType distinguishes types of Colors.

const (
	// ColorTypeRGBA is a direct RGBA color.
	ColorTypeRGBA ColorType = iota

	// ColorTypePaletteIndex is an indirect color, indexing the custom palette.
	ColorTypePaletteIndex

	// ColorTypeCReg is an indirect color, indexing the CREG color registers.
	ColorTypeCReg

	// ColorTypeBlend is an indirect color, blending two other colors.
	ColorTypeBlend
)

type Destination

type Destination interface {
	Reset(viewbox ViewBox, palette [64]color.RGBA)
	CSel() uint8
	SetCSel(cSel uint8)
	NSel() uint8
	SetNSel(nSel uint8)
	SetCReg(adj uint8, incr bool, c Color)
	SetNReg(adj uint8, incr bool, f float32)
	SetLOD(lod0, lod1 float32)

	StartPath(adj uint8, x, y float32)
	ClosePathEndPath()
	ClosePathAbsMoveTo(x, y float32)
	ClosePathRelMoveTo(x, y float32)

	AbsHLineTo(x float32)
	RelHLineTo(x float32)
	AbsVLineTo(y float32)
	RelVLineTo(y float32)
	AbsLineTo(x, y float32)
	RelLineTo(x, y float32)
	AbsSmoothQuadTo(x, y float32)
	RelSmoothQuadTo(x, y float32)
	AbsQuadTo(x1, y1, x, y float32)
	RelQuadTo(x1, y1, x, y float32)
	AbsSmoothCubeTo(x2, y2, x, y float32)
	RelSmoothCubeTo(x2, y2, x, y float32)
	AbsCubeTo(x1, y1, x2, y2, x, y float32)
	RelCubeTo(x1, y1, x2, y2, x, y float32)
	AbsArcTo(rx, ry, xAxisRotation float32, largeArc, sweep bool, x, y float32)
	RelArcTo(rx, ry, xAxisRotation float32, largeArc, sweep bool, x, y float32)
}

Destination handles the actions decoded from an IconVG graphic's opcodes.

When passed to Decode, the first method called (if any) will be Reset. No methods will be called at all if an error is encountered in the encoded form before the metadata is fully decoded.

type DestinationLogger

type DestinationLogger struct {
	Destination
	Alt bool
}

func (*DestinationLogger) AbsArcTo

func (d *DestinationLogger) AbsArcTo(rx, ry, xAxisRotation float32, largeArc, sweep bool, x, y float32)

func (*DestinationLogger) AbsCubeTo

func (d *DestinationLogger) AbsCubeTo(x1, y1, x2, y2, x, y float32)

func (*DestinationLogger) AbsHLineTo

func (d *DestinationLogger) AbsHLineTo(x float32)

func (*DestinationLogger) AbsLineTo

func (d *DestinationLogger) AbsLineTo(x, y float32)

func (*DestinationLogger) AbsQuadTo

func (d *DestinationLogger) AbsQuadTo(x1, y1, x, y float32)

func (*DestinationLogger) AbsSmoothCubeTo

func (d *DestinationLogger) AbsSmoothCubeTo(x2, y2, x, y float32)

func (*DestinationLogger) AbsSmoothQuadTo

func (d *DestinationLogger) AbsSmoothQuadTo(x, y float32)

func (*DestinationLogger) AbsVLineTo

func (d *DestinationLogger) AbsVLineTo(y float32)

func (*DestinationLogger) ClosePathAbsMoveTo

func (d *DestinationLogger) ClosePathAbsMoveTo(x, y float32)

func (*DestinationLogger) ClosePathEndPath

func (d *DestinationLogger) ClosePathEndPath()

func (*DestinationLogger) ClosePathRelMoveTo

func (d *DestinationLogger) ClosePathRelMoveTo(x, y float32)

func (*DestinationLogger) RelArcTo

func (d *DestinationLogger) RelArcTo(rx, ry, xAxisRotation float32, largeArc, sweep bool, x, y float32)

func (*DestinationLogger) RelCubeTo

func (d *DestinationLogger) RelCubeTo(x1, y1, x2, y2, x, y float32)

func (*DestinationLogger) RelHLineTo

func (d *DestinationLogger) RelHLineTo(x float32)

func (*DestinationLogger) RelLineTo

func (d *DestinationLogger) RelLineTo(x, y float32)

func (*DestinationLogger) RelQuadTo

func (d *DestinationLogger) RelQuadTo(x1, y1, x, y float32)

func (*DestinationLogger) RelSmoothCubeTo

func (d *DestinationLogger) RelSmoothCubeTo(x2, y2, x, y float32)

func (*DestinationLogger) RelSmoothQuadTo

func (d *DestinationLogger) RelSmoothQuadTo(x, y float32)

func (*DestinationLogger) RelVLineTo

func (d *DestinationLogger) RelVLineTo(y float32)

func (*DestinationLogger) Reset

func (d *DestinationLogger) Reset(viewbox ViewBox, palette [64]color.RGBA)

func (*DestinationLogger) SetCReg

func (d *DestinationLogger) SetCReg(adj uint8, incr bool, c Color)

func (*DestinationLogger) SetCSel

func (d *DestinationLogger) SetCSel(cSel uint8)

func (*DestinationLogger) SetLOD

func (d *DestinationLogger) SetLOD(lod0, lod1 float32)

func (*DestinationLogger) SetNReg

func (d *DestinationLogger) SetNReg(adj uint8, incr bool, f float32)

func (*DestinationLogger) SetNSel

func (d *DestinationLogger) SetNSel(nSel uint8)

func (*DestinationLogger) StartPath

func (d *DestinationLogger) StartPath(adj uint8, x, y float32)

type Icon

type Icon interface {
	// Name is a unique name of the icon inside your program. e.g. "favicon"
	// It is used to differentiate it from other icons in your program.
	Name() string

	// ViewBox is the ViewBox of the icon.
	ViewBox() ViewBox

	// RenderOn is called to let the icon render itself on
	// a Destination with a list of color.Color overrides.
	RenderOn(dst Destination, col ...color.Color) error
}

Icon is an interface to an icon that can be drawn on a Destination

type Metadata

type Metadata struct {
	ViewBox ViewBox

	// Palette is a 64 color palette. When encoding, it is the suggested
	// palette to place within the IconVG graphic. When decoding, it is either
	// the optional palette passed to Decode, or if no optional palette was
	// given, the suggested palette within the IconVG graphic.
	Palette [64]color.RGBA
}

Metadata is an IconVG's metadata.

type ViewBox

type ViewBox struct {
	MinX, MinY, MaxX, MaxY float32
}

ViewBox is a Rectangle

func (ViewBox) AspectMeet

func (v ViewBox) AspectMeet(dx, dy float32, ax, ay float32) (MinX, MinY, MaxX, MaxY float32)

AspectMeet fits the ViewBox inside a rectangle of size dx,dy maintaining its aspect ratio. The ax, ay argument determine the position of the resized viewbox in the given rectangle. For example ax = Mid, ay = Mid will position the resized viewbox always in the middle of the rectangle.

func (ViewBox) AspectSlice

func (v ViewBox) AspectSlice(dx, dy float32, ax, ay float32) (MinX, MinY, MaxX, MaxY float32)

AspectSlice fills the rectangle of size dx,dy maintaining the ViewBox's aspect ratio. The ax,ay arguments determine the position of the resized viewbox in the given rectangle. For example ax = Mid, ay = Mid will position the resized viewbox always in the middle of the rectangle

func (ViewBox) Size

func (v ViewBox) Size() (dx, dy float32)

Size returns the ViewBox's size in both dimensions. An IconVG graphic is scalable; these dimensions do not necessarily map 1:1 to pixels.

Directories

Path Synopsis
cmd
Package raster provides rasterizers for 2-D vector graphics.
Package raster provides rasterizers for 2-D vector graphics.
vec

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL