mirror of
https://github.com/greenshot/greenshot
synced 2025-07-14 09:03:44 -07:00
Fixed a problem with writing transparent bitmaps to non transparent image formats. Cleaned up some code, implemented the start of a donate page in the installer (also tweaked the size a bit)
git-svn-id: http://svn.code.sf.net/p/greenshot/code/trunk@1673 7dccd23d-a4a3-4e1f-8c07-b4c1b4018ab4
This commit is contained in:
parent
4a07359cf6
commit
150ea9c4eb
7 changed files with 157 additions and 51 deletions
|
@ -76,41 +76,41 @@ namespace Greenshot.Helpers {
|
||||||
/// Saves image to stream with specified quality
|
/// Saves image to stream with specified quality
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static void SaveToStream(Image imageToSave, Stream stream, OutputFormat extension, int quality) {
|
public static void SaveToStream(Image imageToSave, Stream stream, OutputFormat extension, int quality) {
|
||||||
ImageFormat imfo = null;
|
ImageFormat imageFormat = null;
|
||||||
bool disposeImage = false;
|
//bool disposeImage = false;
|
||||||
|
|
||||||
switch (extension) {
|
switch (extension) {
|
||||||
case OutputFormat.bmp:
|
case OutputFormat.bmp:
|
||||||
imfo = ImageFormat.Bmp;
|
imageFormat = ImageFormat.Bmp;
|
||||||
break;
|
break;
|
||||||
case OutputFormat.gif:
|
case OutputFormat.gif:
|
||||||
imfo = ImageFormat.Gif;
|
imageFormat = ImageFormat.Gif;
|
||||||
break;
|
break;
|
||||||
case OutputFormat.jpg:
|
case OutputFormat.jpg:
|
||||||
imfo = ImageFormat.Jpeg;
|
imageFormat = ImageFormat.Jpeg;
|
||||||
break;
|
break;
|
||||||
case OutputFormat.png:
|
case OutputFormat.png:
|
||||||
imfo = ImageFormat.Png;
|
imageFormat = ImageFormat.Png;
|
||||||
break;
|
break;
|
||||||
case OutputFormat.tiff:
|
case OutputFormat.tiff:
|
||||||
imfo = ImageFormat.Tiff;
|
imageFormat = ImageFormat.Tiff;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
imfo = ImageFormat.Png;
|
imageFormat = ImageFormat.Png;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
// If Quantizing is enable, overwrite the image to save with a 256 - color version
|
//// If Quantizing is enable, overwrite the image to save with a 256 - color version
|
||||||
if (conf.OutputFileReduceColors) {
|
//if (conf.OutputFileReduceColors) {
|
||||||
try {
|
// try {
|
||||||
LOG.Debug("Reducing colors on bitmap.");
|
// LOG.Debug("Reducing colors on bitmap.");
|
||||||
Quantizer quantizer = new OctreeQuantizer(255,8);
|
// Quantizer quantizer = new OctreeQuantizer(255,8);
|
||||||
imageToSave = quantizer.Quantize(imageToSave);
|
// imageToSave = quantizer.Quantize(imageToSave);
|
||||||
// Make sure the "new" image is disposed
|
// // Make sure the "new" image is disposed
|
||||||
disposeImage = true;
|
// disposeImage = true;
|
||||||
} catch(Exception e) {
|
// } catch(Exception e) {
|
||||||
LOG.Warn("Error occurred while Quantizing the image, ignoring and using original. Error: ", e);
|
// LOG.Warn("Error occurred while Quantizing the image, ignoring and using original. Error: ", e);
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Create meta-data
|
// Create meta-data
|
||||||
|
@ -119,23 +119,28 @@ namespace Greenshot.Helpers {
|
||||||
try {
|
try {
|
||||||
imageToSave.SetPropertyItem(softwareUsedPropertyItem);
|
imageToSave.SetPropertyItem(softwareUsedPropertyItem);
|
||||||
} catch (ArgumentException) {
|
} catch (ArgumentException) {
|
||||||
LOG.WarnFormat("Image of type {0} do not support property {1}", imfo, softwareUsedPropertyItem.Id);
|
LOG.WarnFormat("Image of type {0} do not support property {1}", imageFormat, softwareUsedPropertyItem.Id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
LOG.DebugFormat("Saving image to stream with PixelFormat {0}", imageToSave.PixelFormat);
|
LOG.DebugFormat("Saving image to stream with PixelFormat {0}", imageToSave.PixelFormat);
|
||||||
if (imfo == ImageFormat.Jpeg) {
|
if (imageFormat == ImageFormat.Jpeg) {
|
||||||
EncoderParameters parameters = new EncoderParameters(1);
|
EncoderParameters parameters = new EncoderParameters(1);
|
||||||
parameters.Param[0] = new System.Drawing.Imaging.EncoderParameter(Encoder.Quality, quality);
|
parameters.Param[0] = new System.Drawing.Imaging.EncoderParameter(Encoder.Quality, quality);
|
||||||
ImageCodecInfo[] ies = ImageCodecInfo.GetImageEncoders();
|
ImageCodecInfo[] ies = ImageCodecInfo.GetImageEncoders();
|
||||||
imageToSave.Save(stream, ies[1], parameters);
|
imageToSave.Save(stream, ies[1], parameters);
|
||||||
|
} else if (imageFormat != ImageFormat.Png && Image.IsAlphaPixelFormat(imageToSave.PixelFormat)) {
|
||||||
|
// No transparency in target format
|
||||||
|
using (Bitmap tmpBitmap = ImageHelper.Clone(imageToSave, PixelFormat.Format24bppRgb)) {
|
||||||
|
tmpBitmap.Save(stream, imageFormat);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
imageToSave.Save(stream, imfo);
|
imageToSave.Save(stream, imageFormat);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
// cleanup if needed
|
// // cleanup if needed
|
||||||
if (disposeImage && imageToSave != null) {
|
// if (disposeImage && imageToSave != null) {
|
||||||
imageToSave.Dispose();
|
// imageToSave.Dispose();
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -142,20 +142,6 @@ namespace Greenshot.Helpers {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Invert Bitmap if wanted
|
|
||||||
if (conf.OutputPrintInverted) {
|
|
||||||
using (BitmapBuffer bb = new BitmapBuffer((Bitmap)image, false)) {
|
|
||||||
bb.Lock();
|
|
||||||
for (int y = 0; y < bb.Height; y++) {
|
|
||||||
for (int x = 0; x < bb.Width; x++) {
|
|
||||||
Color color = bb.GetColorAt(x, y);
|
|
||||||
Color invertedColor = Color.FromArgb(color.A, color.R ^ 255, color.G ^ 255, color.B ^ 255);
|
|
||||||
bb.SetColorAt(x, y, invertedColor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get a rectangle representing the printable Area
|
// Get a rectangle representing the printable Area
|
||||||
RectangleF pageRect = e.PageSettings.PrintableArea;
|
RectangleF pageRect = e.PageSettings.PrintableArea;
|
||||||
|
|
||||||
|
@ -192,8 +178,13 @@ namespace Greenshot.Helpers {
|
||||||
e.Graphics.DrawString(dateString, f, Brushes.Black, pageRect.Width / 2 - (dateStringWidth / 2), pageRect.Height);
|
e.Graphics.DrawString(dateString, f, Brushes.Black, pageRect.Width / 2 - (dateStringWidth / 2), pageRect.Height);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (conf.OutputPrintInverted) {
|
||||||
e.Graphics.DrawImage(image, printRect, imageRect, GraphicsUnit.Pixel);
|
using (Bitmap negativeBitmap = ImageHelper.CreateNegative((Bitmap)image)) {
|
||||||
|
e.Graphics.DrawImage(negativeBitmap, printRect, imageRect, GraphicsUnit.Pixel);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
e.Graphics.DrawImage(image, printRect, imageRect, GraphicsUnit.Pixel);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
BIN
Greenshot/releases/additional_files/donate.bmp
Normal file
BIN
Greenshot/releases/additional_files/donate.bmp
Normal file
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
|
@ -1,10 +1,6 @@
|
||||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc.
|
Copyright (C) 1989, 1991 Free Software Foundation, Inc.
|
||||||
|
|
||||||
59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
|
||||||
Everyone is permitted to copy and distribute verbatim copies
|
|
||||||
|
|
||||||
of this license document, but changing it is not allowed.
|
|
||||||
|
|
||||||
Preamble
|
Preamble
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,6 @@ echo Starting Greenshot BUILD
|
||||||
cd ..
|
cd ..
|
||||||
echo Getting current Version
|
echo Getting current Version
|
||||||
tools\TortoiseSVN\SubWCRev.exe . releases\innosetup\setup.iss releases\innosetup\setup-SVN.iss
|
tools\TortoiseSVN\SubWCRev.exe . releases\innosetup\setup.iss releases\innosetup\setup-SVN.iss
|
||||||
del bin\Release\*.config
|
|
||||||
del bin\Release\*.log
|
del bin\Release\*.log
|
||||||
cd bin\Release
|
cd bin\Release
|
||||||
echo Making MD5
|
echo Making MD5
|
||||||
|
|
|
@ -23,6 +23,7 @@ Source: ..\..\bin\Release\checksum.MD5; DestDir: {app}; Flags: overwritereadonly
|
||||||
Source: ..\additional_files\installer.txt; DestDir: {app}; Flags: overwritereadonly recursesubdirs ignoreversion replacesameversion
|
Source: ..\additional_files\installer.txt; DestDir: {app}; Flags: overwritereadonly recursesubdirs ignoreversion replacesameversion
|
||||||
Source: ..\additional_files\license.txt; DestDir: {app}; Flags: overwritereadonly recursesubdirs ignoreversion replacesameversion
|
Source: ..\additional_files\license.txt; DestDir: {app}; Flags: overwritereadonly recursesubdirs ignoreversion replacesameversion
|
||||||
Source: ..\additional_files\readme.txt; DestDir: {app}; Flags: overwritereadonly recursesubdirs ignoreversion replacesameversion
|
Source: ..\additional_files\readme.txt; DestDir: {app}; Flags: overwritereadonly recursesubdirs ignoreversion replacesameversion
|
||||||
|
Source: ..\additional_files\donate.bmp; Flags: dontcopy
|
||||||
; Core language files
|
; Core language files
|
||||||
Source: ..\..\bin\Release\Languages\*nl-NL*; DestDir: {app}\Languages; Flags: overwritereadonly ignoreversion replacesameversion;
|
Source: ..\..\bin\Release\Languages\*nl-NL*; DestDir: {app}\Languages; Flags: overwritereadonly ignoreversion replacesameversion;
|
||||||
Source: ..\..\bin\Release\Languages\*en-US*; DestDir: {app}\Languages; Flags: overwritereadonly ignoreversion replacesameversion;
|
Source: ..\..\bin\Release\Languages\*en-US*; DestDir: {app}\Languages; Flags: overwritereadonly ignoreversion replacesameversion;
|
||||||
|
@ -89,9 +90,12 @@ AppUpdatesURL=http://getgreenshot.org
|
||||||
AppVerName={#ExeName} {#Version}
|
AppVerName={#ExeName} {#Version}
|
||||||
AppVersion={#Version}
|
AppVersion={#Version}
|
||||||
ArchitecturesInstallIn64BitMode=x64
|
ArchitecturesInstallIn64BitMode=x64
|
||||||
|
Compression=lzma2/ultra64
|
||||||
|
SolidCompression=yes
|
||||||
DefaultDirName={pf}\{#ExeName}
|
DefaultDirName={pf}\{#ExeName}
|
||||||
DefaultGroupName={#ExeName}
|
DefaultGroupName={#ExeName}
|
||||||
InfoBeforeFile=..\additional_files\readme.txt
|
InfoBeforeFile=..\additional_files\readme.txt
|
||||||
|
LicenseFile=..\additional_files\license.txt
|
||||||
LanguageDetectionMethod=uilanguage
|
LanguageDetectionMethod=uilanguage
|
||||||
MinVersion=,5.01.2600
|
MinVersion=,5.01.2600
|
||||||
OutputBaseFilename={#ExeName}-INSTALLER-UNSTABLE-{#Version}
|
OutputBaseFilename={#ExeName}-INSTALLER-UNSTABLE-{#Version}
|
||||||
|
@ -163,6 +167,15 @@ nl.language=Extra talen
|
||||||
en.optimize=Optimizing performance, this may take a while.
|
en.optimize=Optimizing performance, this may take a while.
|
||||||
de.optimize=Optimierung der Leistung, kann etwas dauern.
|
de.optimize=Optimierung der Leistung, kann etwas dauern.
|
||||||
nl.optimize=Prestaties verbeteren, kan even duren.
|
nl.optimize=Prestaties verbeteren, kan even duren.
|
||||||
|
en.supportus_caption=Support Greenshot
|
||||||
|
de.supportus_caption=Unterstutz Greenshot
|
||||||
|
nl.supportus_caption=Ondersteun Greenshot
|
||||||
|
en.supportus_description=Things you can do to support Greenshot
|
||||||
|
de.supportus_description=Was Sie tun können um Greenshot zu unterstutzen
|
||||||
|
nl.supportus_description=Wat U doen kunt om Greenshot te ondersteuen
|
||||||
|
en.supportus_text=We re-donate 10% of every donation to the WWF. The more you donate, the more you help us and the environment. This is what makes Greenshot green.
|
||||||
|
de.supportus_text=We re-donate 10% of every donation to the WWF. The more you donate, the more you help us and the environment. This is what makes Greenshot green.
|
||||||
|
nl.supportus_text=We re-donate 10% of every donation to the WWF. The more you donate, the more you help us and the environment. This is what makes Greenshot green.
|
||||||
[Components]
|
[Components]
|
||||||
Name: "plugins"; Description: "Plugins"; Types: Full
|
Name: "plugins"; Description: "Plugins"; Types: Full
|
||||||
Name: "plugins\ocr"; Description: {cm:ocr}; Types: Full;
|
Name: "plugins\ocr"; Description: {cm:ocr}; Types: Full;
|
||||||
|
@ -196,6 +209,7 @@ Name: "languages\trTR"; Description: "Turkish"; Types: Full; Check: hasLanguageG
|
||||||
Name: "languages\zhCN"; Description: "简体中文"; Types: Full; Check: hasLanguageGroup('a')
|
Name: "languages\zhCN"; Description: "简体中文"; Types: Full; Check: hasLanguageGroup('a')
|
||||||
Name: "languages\zhTW"; Description: "繁體中文"; Types: Full; Check: hasLanguageGroup('9')
|
Name: "languages\zhTW"; Description: "繁體中文"; Types: Full; Check: hasLanguageGroup('9')
|
||||||
[Code]
|
[Code]
|
||||||
|
|
||||||
// Build a list of greenshot parameters from the supplied installer parameters
|
// Build a list of greenshot parameters from the supplied installer parameters
|
||||||
function GetParamsForGS(argument: String): String;
|
function GetParamsForGS(argument: String): String;
|
||||||
var
|
var
|
||||||
|
@ -245,8 +259,8 @@ end;
|
||||||
// Check if language group is installed
|
// Check if language group is installed
|
||||||
function hasLanguageGroup(argument: String): Boolean;
|
function hasLanguageGroup(argument: String): Boolean;
|
||||||
var
|
var
|
||||||
keyValue: String;
|
keyValue: String;
|
||||||
returnValue: Boolean;
|
returnValue: Boolean;
|
||||||
begin
|
begin
|
||||||
returnValue := true;
|
returnValue := true;
|
||||||
if (RegQueryStringValue( HKLM, 'SYSTEM\CurrentControlSet\Control\Nls\Language Groups', argument, keyValue)) then begin
|
if (RegQueryStringValue( HKLM, 'SYSTEM\CurrentControlSet\Control\Nls\Language Groups', argument, keyValue)) then begin
|
||||||
|
@ -257,6 +271,86 @@ begin
|
||||||
Result := returnValue;
|
Result := returnValue;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
// Called if the Donate Image is clicked
|
||||||
|
procedure ImageClick(Sender: TObject);
|
||||||
|
var
|
||||||
|
ErrCode: integer;
|
||||||
|
begin
|
||||||
|
ShellExec('open', 'http://getgreenshot.org/support/', '', '', SW_SHOW, ewNoWait, ErrCode);
|
||||||
|
end;
|
||||||
|
|
||||||
|
// Called if the Donate Button is clicked
|
||||||
|
procedure DonateClick(Sender: TObject);
|
||||||
|
var
|
||||||
|
ErrCode: integer;
|
||||||
|
ClickedButton: TButton;
|
||||||
|
begin
|
||||||
|
ClickedButton := TButton(Sender);
|
||||||
|
ShellExec('open', 'http://sourceforge.net/donate/index.php?group_id=191585&type=0&amt=' + ClickedButton.Caption, '', '', SW_SHOW, ewNoWait, ErrCode);
|
||||||
|
end;
|
||||||
|
|
||||||
|
// Create custom page
|
||||||
|
function CreateSupportUsPage(PreviousPageId: Integer) : Integer;
|
||||||
|
var
|
||||||
|
SupportUsPage: TWizardPage;
|
||||||
|
DonateImage : TBitmapImage;
|
||||||
|
BitmapLocation: string;
|
||||||
|
RichTextViewer : TRichEditViewer;
|
||||||
|
Button1, Button5, Button10: TButton;
|
||||||
|
begin
|
||||||
|
SupportUsPage := CreateCustomPage(PreviousPageId, ExpandConstant('{cm:supportus_caption}'), ExpandConstant('{cm:supportus_description}'));
|
||||||
|
ExtractTemporaryFile('donate.bmp');
|
||||||
|
BitmapLocation := ExpandConstant('{tmp}')+'\donate.bmp';
|
||||||
|
|
||||||
|
// Image
|
||||||
|
DonateImage := TBitmapImage.Create(SupportUsPage);
|
||||||
|
DonateImage.Left := 10;
|
||||||
|
DonateImage.Top := 100;
|
||||||
|
DonateImage.AutoSize := True;
|
||||||
|
DonateImage.Bitmap.LoadFromFile(BitmapLocation);
|
||||||
|
DonateImage.Parent := SupportUsPage.Surface;
|
||||||
|
DonateImage.OnClick := @ImageClick;
|
||||||
|
|
||||||
|
// Donate Buttons
|
||||||
|
Button1 := TButton.Create(SupportUsPage);
|
||||||
|
Button1.Width := 50;
|
||||||
|
Button1.Height := 20;
|
||||||
|
Button1.Left := 20;
|
||||||
|
Button1.Top := 45;
|
||||||
|
Button1.Caption := '1$';
|
||||||
|
Button1.OnClick := @DonateClick;
|
||||||
|
Button1.Parent := SupportUsPage.Surface;
|
||||||
|
|
||||||
|
Button5 := TButton.Create(SupportUsPage);
|
||||||
|
Button5.Width := 50;
|
||||||
|
Button5.Height := 20;
|
||||||
|
Button5.Left := 20;
|
||||||
|
Button5.Top := 70;
|
||||||
|
Button5.Caption := '5$';
|
||||||
|
Button5.OnClick := @DonateClick;
|
||||||
|
Button5.Parent := SupportUsPage.Surface;
|
||||||
|
|
||||||
|
// Text
|
||||||
|
RichTextViewer := TRichEditViewer.Create(SupportUsPage);
|
||||||
|
RichTextViewer.Parent := SupportUsPage.Surface;
|
||||||
|
RichTextViewer.Left := 10;
|
||||||
|
RichTextViewer.Top := 10;
|
||||||
|
RichTextViewer.Width := SupportUsPage.SurfaceWidth;
|
||||||
|
RichTextViewer.Height := 30;
|
||||||
|
RichTextViewer.ReadOnly := True;
|
||||||
|
RichTextViewer.BorderStyle := bsNone;
|
||||||
|
RichTextViewer.Color := clBtnFace;
|
||||||
|
RichTextViewer.RTFText := '{\rtf1\ansi\ansicpg1252\deff0\deflang13322{\fonttbl{\f0\fnil\fcharset0 Tahoma;}}\viewkind4\uc1\pard\f0\fs16 This is a normal text, \b and this is a bold text\b0\par}';
|
||||||
|
Result:= SupportUsPage.Id;
|
||||||
|
end;
|
||||||
|
|
||||||
|
// Create the donate image
|
||||||
|
procedure InitializeWizard();
|
||||||
|
begin
|
||||||
|
CreateSupportUsPage(wpLicense);
|
||||||
|
end;
|
||||||
|
|
||||||
|
// Initialize the setup
|
||||||
function InitializeSetup(): Boolean;
|
function InitializeSetup(): Boolean;
|
||||||
begin
|
begin
|
||||||
// Enhance installer otherwise .NET installations won't work
|
// Enhance installer otherwise .NET installations won't work
|
||||||
|
|
|
@ -216,6 +216,7 @@ namespace GreenshotPlugin.Core {
|
||||||
// We create a copy of the bitmap, so everything else can be disposed
|
// We create a copy of the bitmap, so everything else can be disposed
|
||||||
imageFileStream.Position = 0;
|
imageFileStream.Position = 0;
|
||||||
using (Image tmpImage = Image.FromStream(imageFileStream, true, true)) {
|
using (Image tmpImage = Image.FromStream(imageFileStream, true, true)) {
|
||||||
|
LOG.DebugFormat("Loaded {0} with Size {1}x{2} and PixelFormat {3}", filename, tmpImage.Width, tmpImage.Height, tmpImage.PixelFormat);
|
||||||
fileBitmap = Clone(tmpImage);
|
fileBitmap = Clone(tmpImage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -779,6 +780,26 @@ namespace GreenshotPlugin.Core {
|
||||||
return newImage;
|
return newImage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Return negative of Bitmap
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="sourceBitmap">Bitmap to create a negative off</param>
|
||||||
|
/// <returns>Negative bitmap</returns>
|
||||||
|
public static Bitmap CreateNegative(Bitmap sourceBitmap) {
|
||||||
|
using (BitmapBuffer bb = new BitmapBuffer(sourceBitmap, true)) {
|
||||||
|
bb.Lock();
|
||||||
|
for (int y = 0; y < bb.Height; y++) {
|
||||||
|
for (int x = 0; x < bb.Width; x++) {
|
||||||
|
Color color = bb.GetColorAt(x, y);
|
||||||
|
Color invertedColor = Color.FromArgb(color.A, color.R ^ 255, color.G ^ 255, color.B ^ 255);
|
||||||
|
bb.SetColorAt(x, y, invertedColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bb.Unlock();
|
||||||
|
return bb.Bitmap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a new bitmap where the sourceBitmap has a Simple border around it
|
/// Create a new bitmap where the sourceBitmap has a Simple border around it
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue