Compare commits

..

No commits in common. "724e03176e840051ba2729ba713412c3593806b0" and "5db9199705a968bef634019e31666dd5aab5baf2" have entirely different histories.

33 changed files with 34 additions and 2984 deletions

View File

@ -17,10 +17,7 @@
"WebFetch(domain:github.com)", "WebFetch(domain:github.com)",
"WebFetch(domain:gist.github.com)", "WebFetch(domain:gist.github.com)",
"Bash(dotnet list:*)", "Bash(dotnet list:*)",
"Bash(dotnet build:*)", "Bash(dotnet build:*)"
"Bash(rm:*)",
"Bash(dotnet add package:*)",
"Bash(dir:*)"
], ],
"deny": [], "deny": [],
"ask": [] "ask": []

View File

@ -1,69 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using Convert_It_Online.Services;
using Microsoft.AspNetCore.Localization;
namespace Convert_It_Online.Areas.AudioTools.Controllers
{
[Area("AudioTools")]
[Route("{culture}/[area]/[controller]")]
[Route("[area]/[controller]")] // Adicionado para Share Target sem cultura fixa
public class SpeechToTextController : Controller
{
private readonly IAudioTranscriptionService _transcriptionService;
private readonly ILogger<SpeechToTextController> _logger;
public SpeechToTextController(IAudioTranscriptionService transcriptionService, ILogger<SpeechToTextController> logger)
{
_transcriptionService = transcriptionService;
_logger = logger;
}
[HttpGet]
public IActionResult Index()
{
return View();
}
[HttpPost]
public async Task<IActionResult> Transcribe(IFormFile audioFile)
{
if (audioFile == null || audioFile.Length == 0)
{
ViewBag.Error = "Por favor, selecione um arquivo de áudio.";
return View("Index");
}
var culture = HttpContext.Features.Get<IRequestCultureFeature>()?.RequestCulture.UICulture.Name ?? "pt-BR";
var tempPath = Path.GetTempFileName();
try
{
using (var stream = new FileStream(tempPath, FileMode.Create))
{
await audioFile.CopyToAsync(stream);
}
var transcription = await _transcriptionService.TranscribeAsync(tempPath, culture);
ViewBag.Result = transcription;
}
catch (Exception ex)
{
_logger.LogError(ex, "Erro no controller ao transcrever.");
ViewBag.Error = "Erro ao processar o áudio. Verifique se o formato é suportado.";
}
finally
{
if (System.IO.File.Exists(tempPath)) System.IO.File.Delete(tempPath);
}
return View("Index");
}
[HttpPost("HandleShare")]
public async Task<IActionResult> HandleShare(IFormFile audio)
{
// O Android via Share Target costuma enviar como 'audio' ou 'file'
return await Transcribe(audio);
}
}
}

View File

@ -1,15 +0,0 @@
using Microsoft.AspNetCore.Mvc;
namespace Convert_It_Online.Areas.AudioTools.Controllers
{
[Area("AudioTools")]
[Route("{culture}/[area]/[controller]")]
public class TextToSpeechController : Controller
{
[HttpGet]
public IActionResult Index()
{
return View();
}
}
}

View File

@ -1,69 +0,0 @@
@{
ViewData["Title"] = "Áudio para Texto (Transcrição)";
var culture = ViewContext.RouteData.Values["culture"] as string ?? "pt-BR";
}
<div class="text-center mb-5">
<h1 class="display-4">@ViewData["Title"]</h1>
<p class="lead">Converta áudios do WhatsApp, reuniões ou gravações em texto automaticamente usando IA.</p>
</div>
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card shadow-custom p-4">
<form asp-action="Transcribe" method="post" enctype="multipart/form-data">
<div class="mb-4">
<label for="audioFile" class="form-label h5">Selecione o arquivo de áudio</label>
<input type="file" class="form-control form-control-lg" id="audioFile" name="audioFile" accept="audio/*" required>
<div class="form-text mt-2">Formatos suportados: MP3, WAV, OGG, OPUS, M4A, etc.</div>
</div>
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary btn-lg">
<i class="bi bi-mic-fill me-2"></i>Transcrever Áudio
</button>
</div>
</form>
@if (ViewBag.Error != null)
{
<div class="alert alert-danger mt-4" role="alert">
<i class="bi bi-exclamation-triangle-fill me-2"></i>@ViewBag.Error
</div>
}
@if (ViewBag.Result != null)
{
<div class="mt-5">
<h4 class="mb-3">Transcrição:</h4>
<div class="p-3 bg-light border rounded" style="min-height: 150px; white-space: pre-wrap;">@ViewBag.Result</div>
<div class="mt-3 d-flex gap-2">
<button class="btn btn-outline-secondary btn-sm" onclick="copyTranscription()">
<i class="bi bi-clipboard me-1"></i>Copiar Texto
</button>
</div>
</div>
}
</div>
<div class="mt-5">
<h3 class="h5 mb-3"><i class="bi bi-shield-check me-2"></i>Privacidade e Tecnologia</h3>
<p class="text-muted small">
Seu áudio é processado usando a tecnologia <strong>OpenAI Whisper</strong> rodando diretamente em nosso servidor.
Não enviamos seus dados para APIs externas e os arquivos temporários são deletados imediatamente após a conversão.
</p>
</div>
</div>
</div>
@section Scripts {
<script>
function copyTranscription() {
const text = document.querySelector('.bg-light.border.rounded').innerText;
navigator.clipboard.writeText(text).then(() => {
alert('Transcrição copiada!');
});
}
</script>
}

View File

@ -1,122 +0,0 @@
@{
ViewData["Title"] = "Texto para Áudio (Voz)";
}
<div class="text-center mb-5">
<h1 class="display-4">@ViewData["Title"]</h1>
<p class="lead">Converta qualquer texto em fala usando vozes neurais de alta qualidade.</p>
</div>
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card shadow-custom p-4">
<div class="mb-4">
<label for="textInput" class="form-label h5">Digite ou cole seu texto</label>
<textarea class="form-control" id="textInput" rows="6" placeholder="Escreva aqui o que você deseja que seja lido..."></textarea>
</div>
<div class="row mb-4">
<div class="col-md-6">
<label for="voiceSelect" class="form-label">Escolher Voz</label>
<select id="voiceSelect" class="form-select"></select>
</div>
<div class="col-md-3">
<label for="rate" class="form-label">Velocidade</label>
<input type="range" class="form-range" min="0.5" max="2" step="0.1" id="rate" value="1">
</div>
<div class="col-md-3">
<label for="pitch" class="form-label">Tom</label>
<input type="range" class="form-range" min="0" max="2" step="0.1" id="pitch" value="1">
</div>
</div>
<div class="d-grid gap-2 d-md-flex justify-content-md-center">
<button type="button" class="btn btn-primary btn-lg px-5" onclick="speak()">
<i class="bi bi-play-fill me-2"></i>Ouvir
</button>
<button type="button" class="btn btn-outline-danger btn-lg" onclick="stop()">
<i class="bi bi-stop-fill me-2"></i>Parar
</button>
</div>
</div>
<div class="mt-4 alert alert-info">
<i class="bi bi-info-circle me-2"></i>
Esta ferramenta usa as vozes instaladas no seu dispositivo. No Android e Windows, você encontrará opções de vozes neurais muito naturais.
</div>
</div>
</div>
@section Scripts {
<script>
const synth = window.speechSynthesis;
const voiceSelect = document.querySelector('#voiceSelect');
const textInput = document.querySelector('#textInput');
const rate = document.querySelector('#rate');
const pitch = document.querySelector('#pitch');
let voices = [];
function populateVoiceList() {
voices = synth.getVoices().sort(function (a, b) {
const aname = a.name.toUpperCase();
const bname = b.name.toUpperCase();
if (aname < bname) return -1;
else if (aname > bname) return 1;
else return 0;
});
const selectedIndex = voiceSelect.selectedIndex < 0 ? 0 : voiceSelect.selectedIndex;
voiceSelect.innerHTML = '';
for (let i = 0; i < voices.length; i++) {
const option = document.createElement('option');
option.textContent = voices[i].name + ' (' + voices[i].lang + ')';
if (voices[i].default) {
option.textContent += ' -- PADRÃO';
}
option.setAttribute('data-lang', voices[i].lang);
option.setAttribute('data-name', voices[i].name);
voiceSelect.appendChild(option);
}
voiceSelect.selectedIndex = selectedIndex;
}
populateVoiceList();
if (speechSynthesis.onvoiceschanged !== undefined) {
speechSynthesis.onvoiceschanged = populateVoiceList;
}
function speak() {
if (synth.speaking) {
console.error('speechSynthesis.speaking');
return;
}
if (textInput.value !== '') {
const utterThis = new SpeechSynthesisUtterance(textInput.value);
utterThis.onend = function (event) {
console.log('SpeechSynthesisUtterance.onend');
}
utterThis.onerror = function (event) {
console.error('SpeechSynthesisUtterance.onerror');
}
const selectedOption = voiceSelect.selectedOptions[0].getAttribute('data-name');
for (let i = 0; i < voices.length; i++) {
if (voices[i].name === selectedOption) {
utterThis.voice = voices[i];
break;
}
}
utterThis.pitch = pitch.value;
utterThis.rate = rate.value;
synth.speak(utterThis);
}
}
function stop() {
synth.cancel();
}
</script>
}

View File

@ -1,5 +0,0 @@
@using Convert_It_Online
@using Microsoft.AspNetCore.Mvc.Localization
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@inject IViewLocalizer Localizer

View File

@ -1,3 +0,0 @@
@{
Layout = "_Layout";
}

View File

@ -1,319 +0,0 @@
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Localization;
using iText.Kernel.Pdf;
using iText.Kernel.Pdf.Canvas.Parser;
using iText.Kernel.Pdf.Canvas.Parser.Listener;
using iText.Kernel.Exceptions;
namespace Convert_It_Online.Areas.DocumentConverters.Controllers
{
[Area("DocumentConverters")]
public class PdfBarcodeLineController : Controller
{
private readonly IStringLocalizer<SharedResource> _localizer;
private readonly ILogger<PdfBarcodeLineController> _logger;
private const long MaxPreviewSize = 5 * 1024 * 1024; // 5MB
private static readonly Regex CandidatePattern = new(@"[0-9\s\.-]{44,70}", RegexOptions.Compiled);
public PdfBarcodeLineController(IStringLocalizer<SharedResource> localizer, ILogger<PdfBarcodeLineController> logger)
{
_localizer = localizer;
_logger = logger;
}
private void SetCommonViewBagProperties()
{
ViewBag.HomeLink = _localizer["HomeLink"];
ViewBag.TextMenuTitle = _localizer["TextMenuTitle"];
ViewBag.ImageMenuTitle = _localizer["ImageMenuTitle"];
ViewBag.DocumentMenuTitle = _localizer["DocumentMenuTitle"];
ViewBag.CaseConverterTitle = _localizer["CaseConverterTitle"];
ViewBag.JpgToWebpTitle = _localizer["JpgToWebpTitle"];
ViewBag.HeicToJpgTitle = _localizer["HeicToJpgTitle"];
ViewBag.PdfToTextTitle = _localizer["PdfToTextTitle"];
ViewBag.PdfBarcodeTitle = _localizer["PdfBarcodeTitle"];
ViewBag.FooterText = _localizer["FooterText"];
ViewBag.About = _localizer["About"];
ViewBag.Contact = _localizer["Contact"];
ViewBag.Terms = _localizer["Terms"];
}
private void PrepareIndexView()
{
SetCommonViewBagProperties();
ViewBag.PageTitle = _localizer["PdfBarcodeConverterPageTitle"];
ViewBag.PageDescription = _localizer["PdfBarcodeConverterPageDescription"];
ViewBag.PdfBarcodeDetectTabTitle = _localizer["PdfBarcodeDetectTabTitle"];
ViewBag.PdfBarcodeDownloadTabTitle = _localizer["PdfBarcodeDownloadTabTitle"];
ViewBag.PdfBarcodeFileInputLabel = _localizer["PdfBarcodeFileInputLabel"];
ViewBag.PdfPasswordLabel = _localizer["PdfPasswordLabel"];
ViewBag.PdfPasswordPlaceholder = _localizer["PdfPasswordPlaceholder"];
ViewBag.PdfBarcodePasswordHint = _localizer["PdfBarcodePasswordHint"];
ViewBag.DetectBarcodeButton = _localizer["DetectBarcodeButton"];
ViewBag.DownloadBarcodeButton = _localizer["DownloadBarcodeButton"];
ViewBag.PdfBarcodePreviewTitle = _localizer["PdfBarcodePreviewTitle"];
ViewBag.SelectFileError = _localizer["SelectFileError"];
ViewBag.CopyButtonLabel = _localizer["CopyButtonLabel"];
ViewBag.CopyButtonSuccess = _localizer["CopyButtonSuccess"];
ViewBag.FaqWhatTitle = _localizer["PdfBarcodeFaqWhatTitle"];
ViewBag.FaqWhatContent = _localizer["PdfBarcodeFaqWhatContent"];
ViewBag.FaqHowTitle = _localizer["PdfBarcodeFaqHowTitle"];
ViewBag.FaqHowContent = _localizer["PdfBarcodeFaqHowContent"];
ViewBag.FaqWhyTitle = _localizer["PdfBarcodeFaqWhyTitle"];
ViewBag.FaqWhyContent = _localizer["PdfBarcodeFaqWhyContent"];
ViewBag.FaqSecurityTitle = _localizer["PdfBarcodeFaqSecurityTitle"];
ViewBag.FaqSecurityContent = _localizer["PdfBarcodeFaqSecurityContent"];
ViewBag.FaqLimitsTitle = _localizer["PdfBarcodeFaqLimitsTitle"];
ViewBag.FaqLimitsContent = _localizer["PdfBarcodeFaqLimitsContent"];
ViewBag.MetaDescription = ViewBag.PageDescription;
}
public IActionResult Index()
{
PrepareIndexView();
return View();
}
[HttpPost]
public async Task<IActionResult> DetectLine(IFormFile pdfFile, string? password, bool preview = false)
{
return await HandleDetection(pdfFile, password, preview, forDownload: false);
}
[HttpPost]
public async Task<IActionResult> DownloadLine(IFormFile pdfFile, string? password, bool preview = false)
{
return await HandleDetection(pdfFile, password, preview, forDownload: true);
}
private async Task<IActionResult> HandleDetection(IFormFile? pdfFile, string? password, bool preview, bool forDownload)
{
if (pdfFile == null || pdfFile.Length == 0)
{
_logger.LogWarning("[PDF-BARCODE] Attempt without file");
if (preview)
{
return Json(new { success = false, message = _localizer["SelectFileError"].Value });
}
ModelState.AddModelError("pdfFile", _localizer["SelectFileError"]);
PrepareIndexView();
return View("Index");
}
if (!IsValidPdf(pdfFile))
{
_logger.LogWarning("[PDF-BARCODE] Invalid file type: {ContentType}", pdfFile.ContentType);
if (preview)
{
return Json(new { success = false, message = _localizer["InvalidPdfFileError"].Value });
}
ModelState.AddModelError("pdfFile", _localizer["InvalidPdfFileError"]);
PrepareIndexView();
return View("Index");
}
if (preview && pdfFile.Length > MaxPreviewSize)
{
return Json(new { success = false, message = _localizer["PdfPreviewTooLarge"].Value });
}
var extraction = await TryExtractTextAsync(pdfFile, password);
if (!extraction.Success)
{
var message = _localizer[extraction.ErrorKey].Value;
if (preview)
{
return Json(new { success = false, message });
}
ModelState.AddModelError("pdfFile", _localizer[extraction.ErrorKey]);
ViewBag.ConversionError = message;
PrepareIndexView();
return View("Index");
}
var lineResult = FindBarcodeLine(extraction.Content ?? string.Empty);
if (lineResult == null)
{
var message = _localizer["PdfNoBarcodeFound"].Value;
if (preview)
{
return Json(new { success = false, message });
}
ModelState.AddModelError("pdfFile", _localizer["PdfNoBarcodeFound"]);
ViewBag.ConversionError = message;
PrepareIndexView();
return View("Index");
}
if (preview)
{
return Json(new
{
success = true,
content = lineResult,
format = "digit-line"
});
}
var fileBaseName = Path.GetFileNameWithoutExtension(pdfFile.FileName);
var downloadFileName = string.IsNullOrWhiteSpace(fileBaseName) ? "linha-digitavel.txt" : fileBaseName + "-linha-digitavel.txt";
var payload = Encoding.UTF8.GetBytes(lineResult + Environment.NewLine);
return File(payload, "text/plain", downloadFileName);
}
private async Task<(bool Success, string? Content, string ErrorKey)> TryExtractTextAsync(IFormFile pdfFile, string? password)
{
try
{
await using var memoryStream = new MemoryStream();
await pdfFile.CopyToAsync(memoryStream);
var pdfBytes = memoryStream.ToArray();
// Try multiple password encodings
var passwords = new List<byte[]?> { null }; // Start with no password
if (!string.IsNullOrEmpty(password))
{
passwords.Add(System.Text.Encoding.UTF8.GetBytes(password));
passwords.Add(System.Text.Encoding.ASCII.GetBytes(password));
passwords.Add(System.Text.Encoding.Latin1.GetBytes(password));
}
foreach (var pwd in passwords)
{
try
{
ReaderProperties readerProperties = new ReaderProperties();
if (pwd != null)
{
readerProperties.SetPassword(pwd);
}
using var pdfReader = new PdfReader(new MemoryStream(pdfBytes), readerProperties);
using var pdfDocument = new PdfDocument(pdfReader);
var builder = new StringBuilder();
int numberOfPages = pdfDocument.GetNumberOfPages();
for (int i = 1; i <= numberOfPages; i++)
{
var page = pdfDocument.GetPage(i);
// Try multiple extraction strategies for barcode detection
var strategies = new ITextExtractionStrategy[]
{
new LocationTextExtractionStrategy(),
new SimpleTextExtractionStrategy()
};
foreach (var strategy in strategies)
{
try
{
var pageText = PdfTextExtractor.GetTextFromPage(page, strategy);
if (!string.IsNullOrWhiteSpace(pageText))
{
builder.AppendLine(pageText);
break;
}
}
catch
{
continue;
}
}
}
var result = builder.ToString();
if (!string.IsNullOrWhiteSpace(result))
{
return (true, result, string.Empty);
}
}
catch (BadPasswordException) when (pwd != null)
{
continue; // Try next password encoding
}
catch (BadPasswordException) when (pwd == null && !string.IsNullOrEmpty(password))
{
break;
}
}
if (!string.IsNullOrEmpty(password))
{
return (false, null, "PdfInvalidPassword");
}
return (false, null, "InvalidPdfFileError");
}
catch (BadPasswordException)
{
return (false, null, string.IsNullOrEmpty(password) ? "PdfPasswordRequired" : "PdfInvalidPassword");
}
catch (Exception ex)
{
_logger.LogError(ex, "[PDF-BARCODE] Failed to extract text from {FileName}", pdfFile.FileName);
return (false, null, "InvalidPdfFileError");
}
}
private static bool IsValidPdf(IFormFile file)
{
if (file == null)
{
return false;
}
var contentType = file.ContentType?.ToLowerInvariant();
if (contentType == "application/pdf" || contentType == "application/x-pdf")
{
return true;
}
return Path.GetExtension(file.FileName).Equals(".pdf", StringComparison.OrdinalIgnoreCase);
}
private static string? FindBarcodeLine(string rawText)
{
if (string.IsNullOrWhiteSpace(rawText))
{
return null;
}
foreach (Match match in CandidatePattern.Matches(rawText))
{
var digits = Regex.Replace(match.Value, "[^0-9]", string.Empty);
if (digits.Length == 47 || digits.Length == 48 || digits.Length == 44)
{
return digits;
}
}
// fallback: check full text digits
var fullDigits = Regex.Replace(rawText, "[^0-9]", string.Empty);
if (fullDigits.Length >= 44)
{
var candidates = new[] { 48, 47, 44 };
foreach (var length in candidates)
{
if (fullDigits.Length >= length)
{
return fullDigits.Substring(0, length);
}
}
}
return null;
}
}
}

View File

@ -1,318 +0,0 @@
using System.IO;
using System.Text;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Localization;
using iText.Kernel.Pdf;
using iText.Kernel.Pdf.Canvas.Parser;
using iText.Kernel.Pdf.Canvas.Parser.Listener;
using iText.Kernel.Exceptions;
namespace Convert_It_Online.Areas.DocumentConverters.Controllers
{
[Area("DocumentConverters")]
public class PdfToTextController : Controller
{
private readonly IStringLocalizer<SharedResource> _localizer;
private readonly ILogger<PdfToTextController> _logger;
private const long MaxPreviewSize = 10 * 1024 * 1024; // 10MB
public PdfToTextController(IStringLocalizer<SharedResource> localizer, ILogger<PdfToTextController> logger)
{
_localizer = localizer;
_logger = logger;
}
private void SetCommonViewBagProperties()
{
ViewBag.HomeLink = _localizer["HomeLink"];
ViewBag.TextMenuTitle = _localizer["TextMenuTitle"];
ViewBag.ImageMenuTitle = _localizer["ImageMenuTitle"];
ViewBag.DocumentMenuTitle = _localizer["DocumentMenuTitle"];
ViewBag.CaseConverterTitle = _localizer["CaseConverterTitle"];
ViewBag.JpgToWebpTitle = _localizer["JpgToWebpTitle"];
ViewBag.HeicToJpgTitle = _localizer["HeicToJpgTitle"];
ViewBag.PdfToTextTitle = _localizer["PdfToTextTitle"];
ViewBag.PdfBarcodeTitle = _localizer["PdfBarcodeTitle"];
ViewBag.FooterText = _localizer["FooterText"];
ViewBag.About = _localizer["About"];
ViewBag.Contact = _localizer["Contact"];
ViewBag.Terms = _localizer["Terms"];
}
private void PrepareIndexView()
{
SetCommonViewBagProperties();
ViewBag.PageTitle = _localizer["PdfTextConverterPageTitle"];
ViewBag.PageDescription = _localizer["PdfTextConverterPageDescription"];
ViewBag.PdfPlainTextTabTitle = _localizer["PdfPlainTextTabTitle"];
ViewBag.PdfMarkdownTabTitle = _localizer["PdfMarkdownTabTitle"];
ViewBag.PdfFileInputLabel = _localizer["PdfFileInputLabel"];
ViewBag.PdfPasswordLabel = _localizer["PdfPasswordLabel"];
ViewBag.PdfPasswordPlaceholder = _localizer["PdfPasswordPlaceholder"];
ViewBag.PdfPasswordHint = _localizer["PdfPasswordHint"];
ViewBag.ExtractPlainTextButton = _localizer["ExtractPlainTextButton"];
ViewBag.ExtractMarkdownButton = _localizer["ExtractMarkdownButton"];
ViewBag.DownloadPlainTextButton = _localizer["DownloadPlainTextButton"];
ViewBag.DownloadMarkdownButton = _localizer["DownloadMarkdownButton"];
ViewBag.PdfTextPreviewTitle = _localizer["PdfTextPreviewTitle"];
ViewBag.SelectFileError = _localizer["SelectFileError"];
ViewBag.FaqWhatTitle = _localizer["PdfTextFaqWhatTitle"];
ViewBag.FaqWhatContent = _localizer["PdfTextFaqWhatContent"];
ViewBag.FaqHowTitle = _localizer["PdfTextFaqHowTitle"];
ViewBag.FaqHowContent = _localizer["PdfTextFaqHowContent"];
ViewBag.FaqWhyTitle = _localizer["PdfTextFaqWhyTitle"];
ViewBag.FaqWhyContent = _localizer["PdfTextFaqWhyContent"];
ViewBag.FaqSecurityTitle = _localizer["PdfTextFaqSecurityTitle"];
ViewBag.FaqSecurityContent = _localizer["PdfTextFaqSecurityContent"];
ViewBag.FaqLimitsTitle = _localizer["PdfTextFaqLimitsTitle"];
ViewBag.FaqLimitsContent = _localizer["PdfTextFaqLimitsContent"];
ViewBag.MetaDescription = ViewBag.PageDescription;
}
public IActionResult Index()
{
PrepareIndexView();
return View();
}
[HttpGet]
public IActionResult Test()
{
return Json(new { success = true, message = "Roteamento funcionando!", timestamp = DateTime.Now });
}
[HttpPost]
public async Task<IActionResult> ExtractPlainText(IFormFile pdfFile, string? password, bool preview = false)
{
return await HandleExtraction(pdfFile, password, preview, toMarkdown: false);
}
[HttpPost]
public async Task<IActionResult> ExtractMarkdown(IFormFile pdfFile, string? password, bool preview = false)
{
return await HandleExtraction(pdfFile, password, preview, toMarkdown: true);
}
private async Task<IActionResult> HandleExtraction(IFormFile? pdfFile, string? password, bool preview, bool toMarkdown)
{
if (pdfFile == null || pdfFile.Length == 0)
{
_logger.LogWarning("[PDF-TEXT] Attempt without file");
if (preview)
{
return Json(new { success = false, message = _localizer["SelectFileError"].Value });
}
ModelState.AddModelError("pdfFile", _localizer["SelectFileError"]);
PrepareIndexView();
return View("Index");
}
if (!IsValidPdf(pdfFile))
{
_logger.LogWarning("[PDF-TEXT] Invalid file type: {ContentType}", pdfFile.ContentType);
if (preview)
{
return Json(new { success = false, message = _localizer["InvalidPdfFileError"].Value });
}
ModelState.AddModelError("pdfFile", _localizer["InvalidPdfFileError"]);
PrepareIndexView();
return View("Index");
}
if (preview && pdfFile.Length > MaxPreviewSize)
{
if (preview)
{
return Json(new { success = false, message = _localizer["PdfPreviewTooLarge"].Value });
}
}
var extraction = await TryExtractTextAsync(pdfFile, password);
if (!extraction.Success)
{
var message = _localizer[extraction.ErrorKey].Value;
if (preview)
{
return Json(new { success = false, message });
}
ModelState.AddModelError("pdfFile", _localizer[extraction.ErrorKey]);
ViewBag.ConversionError = message;
PrepareIndexView();
return View("Index");
}
var textContent = extraction.Content ?? string.Empty;
if (toMarkdown)
{
textContent = ToMarkdown(textContent);
}
var fileBaseName = Path.GetFileNameWithoutExtension(pdfFile.FileName);
var extension = toMarkdown ? ".md" : ".txt";
var downloadFileName = string.IsNullOrWhiteSpace(fileBaseName) ? (toMarkdown ? "resultado.md" : "resultado.txt") : fileBaseName + extension;
var contentType = toMarkdown ? "text/markdown" : "text/plain";
if (preview)
{
return Json(new
{
success = true,
content = textContent,
filename = downloadFileName,
format = toMarkdown ? "markdown" : "text"
});
}
var payload = Encoding.UTF8.GetBytes(textContent);
return File(payload, contentType, downloadFileName);
}
private async Task<(bool Success, string? Content, string ErrorKey)> TryExtractTextAsync(IFormFile pdfFile, string? password)
{
try
{
await using var memoryStream = new MemoryStream();
await pdfFile.CopyToAsync(memoryStream);
var pdfBytes = memoryStream.ToArray();
// Try multiple password encodings and extraction strategies
var passwords = new List<byte[]?> { null }; // Start with no password
if (!string.IsNullOrEmpty(password))
{
passwords.Add(System.Text.Encoding.UTF8.GetBytes(password));
passwords.Add(System.Text.Encoding.ASCII.GetBytes(password));
passwords.Add(System.Text.Encoding.Latin1.GetBytes(password));
}
foreach (var pwd in passwords)
{
try
{
ReaderProperties readerProperties = new ReaderProperties();
if (pwd != null)
{
readerProperties.SetPassword(pwd);
}
using var pdfReader = new PdfReader(new MemoryStream(pdfBytes), readerProperties);
using var pdfDocument = new PdfDocument(pdfReader);
var builder = new StringBuilder();
int numberOfPages = pdfDocument.GetNumberOfPages();
for (int i = 1; i <= numberOfPages; i++)
{
var page = pdfDocument.GetPage(i);
// Try multiple extraction strategies
var strategies = new ITextExtractionStrategy[]
{
new LocationTextExtractionStrategy(),
new SimpleTextExtractionStrategy()
};
string pageText = "";
foreach (var strategy in strategies)
{
try
{
pageText = PdfTextExtractor.GetTextFromPage(page, strategy);
if (!string.IsNullOrWhiteSpace(pageText))
break;
}
catch
{
continue;
}
}
if (!string.IsNullOrWhiteSpace(pageText))
{
builder.AppendLine(pageText);
builder.AppendLine();
}
else
{
builder.AppendLine($"[Página {i} - texto não detectado ou pode conter apenas imagens]");
builder.AppendLine();
}
}
var result = builder.ToString().Trim();
if (!string.IsNullOrWhiteSpace(result))
{
return (true, result, string.Empty);
}
}
catch (BadPasswordException) when (pwd != null)
{
continue; // Try next password encoding
}
catch (BadPasswordException) when (pwd == null && !string.IsNullOrEmpty(password))
{
// Document requires password but none worked
break;
}
}
// If we get here, either password was wrong or no text found
if (!string.IsNullOrEmpty(password))
{
return (false, null, "PdfInvalidPassword");
}
return (false, null, "InvalidPdfFileError");
}
catch (BadPasswordException)
{
return (false, null, string.IsNullOrEmpty(password) ? "PdfPasswordRequired" : "PdfInvalidPassword");
}
catch (Exception ex)
{
_logger.LogError(ex, "[PDF-TEXT] Failed to extract text from {FileName}", pdfFile.FileName);
return (false, null, "InvalidPdfFileError");
}
}
private static bool IsValidPdf(IFormFile file)
{
if (file == null)
{
return false;
}
var contentType = file.ContentType?.ToLowerInvariant();
if (contentType == "application/pdf" || contentType == "application/x-pdf")
{
return true;
}
return Path.GetExtension(file.FileName).Equals(".pdf", StringComparison.OrdinalIgnoreCase);
}
private static string ToMarkdown(string text)
{
var normalized = text.Replace("\r\n", "\n").Trim();
if (string.IsNullOrWhiteSpace(normalized))
{
return string.Empty;
}
var builder = new StringBuilder();
var lines = normalized.Split('\n');
foreach (var line in lines)
{
var trimmed = line.TrimEnd();
builder.AppendLine(trimmed);
}
return builder.ToString().Trim();
}
}
}

View File

@ -1,354 +0,0 @@
@{
ViewData["Title"] = ViewBag.PageTitle;
Layout = "_Layout";
}
<div class="container-fluid">
<div class="row">
<div class="col-12">
<div class="text-center mb-4">
<h1 class="display-5">@ViewBag.PageTitle</h1>
<p class="lead text-muted">@ViewBag.PageDescription</p>
</div>
</div>
</div>
<div class="row justify-content-center">
<div class="col-lg-10 col-xl-8">
<div class="card shadow-sm">
<div class="card-header bg-primary text-white">
<h5 class="mb-0">
<i class="bi bi-upc-scan me-2"></i>@ViewBag.DocumentMenuTitle
</h5>
</div>
<div class="card-body">
@if (ViewBag.ConversionError != null)
{
<div class="alert alert-danger" role="alert">
<i class="bi bi-exclamation-triangle-fill me-2"></i>
@ViewBag.ConversionError
</div>
}
<ul class="nav nav-tabs mb-4" id="barcodeTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="detect-tab" data-bs-toggle="tab" data-bs-target="#detect" type="button" role="tab" aria-controls="detect" aria-selected="true">
<i class="bi bi-eyeglasses me-2"></i>@ViewBag.PdfBarcodeDetectTabTitle
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="download-tab" data-bs-toggle="tab" data-bs-target="#download" type="button" role="tab" aria-controls="download" aria-selected="false">
<i class="bi bi-download me-2"></i>@ViewBag.PdfBarcodeDownloadTabTitle
</button>
</li>
</ul>
<div class="tab-content" id="barcodeTabContent">
<div class="tab-pane fade show active" id="detect" role="tabpanel" aria-labelledby="detect-tab">
<form id="detectForm" action="/pt-BR/DocumentConverters/PdfBarcodeLine/DetectLine" method="post" enctype="multipart/form-data" class="needs-validation" novalidate>
<div asp-validation-summary="ModelOnly" class="alert alert-danger" role="alert"></div>
<div class="mb-4">
<label for="detectPdfFile" class="form-label fw-semibold">@ViewBag.PdfBarcodeFileInputLabel</label>
<input class="form-control" type="file" id="detectPdfFile" name="pdfFile" accept=".pdf" required>
<div class="invalid-feedback">
@ViewBag.SelectFileError
</div>
</div>
<div class="mb-4">
<label for="detectPassword" class="form-label fw-semibold">@ViewBag.PdfPasswordLabel</label>
<input class="form-control" type="password" id="detectPassword" name="password" placeholder="@ViewBag.PdfPasswordPlaceholder">
<div class="form-text">
<i class="bi bi-shield-lock me-1"></i>@ViewBag.PdfBarcodePasswordHint
</div>
</div>
<div class="d-grid gap-2">
<button type="button" id="previewDetectBtn" class="btn btn-primary">
<i class="bi bi-upc-scan me-2"></i>@ViewBag.DetectBarcodeButton
</button>
<button type="submit" class="btn btn-outline-secondary">
<i class="bi bi-download me-2"></i>@ViewBag.DownloadBarcodeButton
</button>
</div>
</form>
</div>
<div class="tab-pane fade" id="download" role="tabpanel" aria-labelledby="download-tab">
<form id="downloadForm" action="/pt-BR/DocumentConverters/PdfBarcodeLine/DownloadLine" method="post" enctype="multipart/form-data" class="needs-validation" novalidate>
<div asp-validation-summary="ModelOnly" class="alert alert-danger" role="alert"></div>
<div class="mb-4">
<label for="downloadPdfFile" class="form-label fw-semibold">@ViewBag.PdfBarcodeFileInputLabel</label>
<input class="form-control" type="file" id="downloadPdfFile" name="pdfFile" accept=".pdf" required>
<div class="invalid-feedback">
@ViewBag.SelectFileError
</div>
</div>
<div class="mb-4">
<label for="downloadPassword" class="form-label fw-semibold">@ViewBag.PdfPasswordLabel</label>
<input class="form-control" type="password" id="downloadPassword" name="password" placeholder="@ViewBag.PdfPasswordPlaceholder">
<div class="form-text">
<i class="bi bi-shield-lock me-1"></i>@ViewBag.PdfBarcodePasswordHint
</div>
</div>
<div class="d-grid gap-2">
<button type="button" id="previewDownloadBtn" class="btn btn-primary">
<i class="bi bi-eye me-2"></i>@ViewBag.DetectBarcodeButton
</button>
<button type="submit" class="btn btn-outline-secondary">
<i class="bi bi-download me-2"></i>@ViewBag.DownloadBarcodeButton
</button>
</div>
</form>
</div>
</div>
<div id="barcodeLoading" class="text-center my-4" style="display: none;">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Carregando...</span>
</div>
</div>
<div id="barcodePreview" class="mt-4" style="display: none;">
<h5 class="mb-3">@ViewBag.PdfBarcodePreviewTitle</h5>
<div class="input-group">
<input type="text" id="barcodeResult" class="form-control" readonly>
<button id="copyBarcodeBtn" class="btn btn-outline-success" type="button">
<i class="bi bi-clipboard-check me-1"></i>@ViewBag.CopyButtonLabel
</button>
</div>
<div class="text-end mt-3">
<button id="downloadBarcodeBtn" class="btn btn-success btn-sm">
<i class="bi bi-box-arrow-down me-2"></i>@ViewBag.DownloadBarcodeButton
</button>
</div>
</div>
<div id="barcodeError" class="mt-4" style="display: none;">
<div class="alert alert-warning" role="alert">
<i class="bi bi-exclamation-triangle me-2"></i>
<span id="barcodeErrorMessage"></span>
<button id="fallbackBarcodeBtn" class="btn btn-outline-primary btn-sm ms-2">
@ViewBag.DownloadBarcodeButton
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row mt-5">
<div class="col-lg-10 mx-auto">
<div class="converter-faq">
<h3 class="h4 mb-3 text-center">Perguntas Frequentes</h3>
<div class="accordion" id="barcodeFaqAccordion">
<div class="accordion-item">
<h2 class="accordion-header" id="barcodeFaqWhat">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#barcodeCollapseWhat" aria-expanded="false" aria-controls="barcodeCollapseWhat">
<i class="bi bi-question-circle me-2"></i>@ViewBag.FaqWhatTitle
</button>
</h2>
<div id="barcodeCollapseWhat" class="accordion-collapse collapse" aria-labelledby="barcodeFaqWhat" data-bs-parent="#barcodeFaqAccordion">
<div class="accordion-body">
@ViewBag.FaqWhatContent
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" id="barcodeFaqHow">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#barcodeCollapseHow" aria-expanded="false" aria-controls="barcodeCollapseHow">
<i class="bi bi-gear me-2"></i>@ViewBag.FaqHowTitle
</button>
</h2>
<div id="barcodeCollapseHow" class="accordion-collapse collapse" aria-labelledby="barcodeFaqHow" data-bs-parent="#barcodeFaqAccordion">
<div class="accordion-body">
@ViewBag.FaqHowContent
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" id="barcodeFaqWhy">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#barcodeCollapseWhy" aria-expanded="false" aria-controls="barcodeCollapseWhy">
<i class="bi bi-lightbulb me-2"></i>@ViewBag.FaqWhyTitle
</button>
</h2>
<div id="barcodeCollapseWhy" class="accordion-collapse collapse" aria-labelledby="barcodeFaqWhy" data-bs-parent="#barcodeFaqAccordion">
<div class="accordion-body">
@ViewBag.FaqWhyContent
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" id="barcodeFaqSecurity">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#barcodeCollapseSecurity" aria-expanded="false" aria-controls="barcodeCollapseSecurity">
<i class="bi bi-shield-check me-2"></i>@ViewBag.FaqSecurityTitle
</button>
</h2>
<div id="barcodeCollapseSecurity" class="accordion-collapse collapse" aria-labelledby="barcodeFaqSecurity" data-bs-parent="#barcodeFaqAccordion">
<div class="accordion-body">
@ViewBag.FaqSecurityContent
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" id="barcodeFaqLimits">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#barcodeCollapseLimits" aria-expanded="false" aria-controls="barcodeCollapseLimits">
<i class="bi bi-exclamation-triangle me-2"></i>@ViewBag.FaqLimitsTitle
</button>
</h2>
<div id="barcodeCollapseLimits" class="accordion-collapse collapse" aria-labelledby="barcodeFaqLimits" data-bs-parent="#barcodeFaqAccordion">
<div class="accordion-body">
@ViewBag.FaqLimitsContent
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<style>
#barcodeResult {
font-family: monospace;
}
.spinner-border {
width: 3rem;
height: 3rem;
}
</style>
@section Scripts {
<script>
(function () {
const detectForm = document.getElementById('detectForm');
const downloadForm = document.getElementById('downloadForm');
const previewDetectBtn = document.getElementById('previewDetectBtn');
const previewDownloadBtn = document.getElementById('previewDownloadBtn');
const downloadBarcodeBtn = document.getElementById('downloadBarcodeBtn');
const fallbackBarcodeBtn = document.getElementById('fallbackBarcodeBtn');
const copyBarcodeBtn = document.getElementById('copyBarcodeBtn');
const barcodeLoading = document.getElementById('barcodeLoading');
const barcodePreview = document.getElementById('barcodePreview');
const barcodeError = document.getElementById('barcodeError');
const barcodeResult = document.getElementById('barcodeResult');
const barcodeErrorMessage = document.getElementById('barcodeErrorMessage');
let currentLine = '';
let currentFileName = 'linha-digitavel.txt';
const forms = document.querySelectorAll('.needs-validation');
Array.from(forms).forEach(function (form) {
form.addEventListener('submit', function (event) {
if (!form.checkValidity()) {
event.preventDefault();
event.stopPropagation();
}
form.classList.add('was-validated');
});
});
previewDetectBtn.addEventListener('click', function () {
handlePreview(detectForm);
});
previewDownloadBtn.addEventListener('click', function () {
handlePreview(downloadForm);
});
downloadBarcodeBtn.addEventListener('click', function () {
if (!currentLine) {
return;
}
const blob = new Blob([currentLine + '\n'], { type: 'text/plain;charset=utf-8' });
const url = window.URL.createObjectURL(blob);
const anchor = document.createElement('a');
anchor.href = url;
anchor.download = currentFileName;
document.body.appendChild(anchor);
anchor.click();
document.body.removeChild(anchor);
window.URL.revokeObjectURL(url);
});
fallbackBarcodeBtn.addEventListener('click', function () {
const activeTab = document.querySelector('.nav-link.active');
const currentForm = activeTab.id === 'detect-tab' ? detectForm : downloadForm;
currentForm.submit();
});
copyBarcodeBtn.addEventListener('click', function () {
if (!currentLine) {
return;
}
navigator.clipboard.writeText(currentLine).then(function () {
copyBarcodeBtn.innerHTML = '<i class="bi bi-clipboard-check-fill me-1"></i>@ViewBag.CopyButtonSuccess';
setTimeout(function () {
copyBarcodeBtn.innerHTML = '<i class="bi bi-clipboard-check me-1"></i>@ViewBag.CopyButtonLabel';
}, 1500);
});
});
function handlePreview(form) {
if (!form.checkValidity()) {
form.classList.add('was-validated');
return;
}
showLoading();
const formData = new FormData(form);
fetch(form.action + '?preview=true', {
method: 'POST',
body: formData
}).then(async response => {
if (!response.ok) {
const text = await response.text();
throw new Error(text || response.statusText);
}
const result = await response.json();
if (!result.success) {
throw new Error(result.message || 'Erro na conversão');
}
currentLine = result.content;
currentFileName = (form === downloadForm ? 'linha-digitavel-download.txt' : 'linha-digitavel.txt');
barcodeResult.value = currentLine;
showPreview();
}).catch(error => {
showError(error.message || 'Erro inesperado.');
});
}
function showLoading() {
barcodeLoading.style.display = 'block';
barcodePreview.style.display = 'none';
barcodeError.style.display = 'none';
}
function showPreview() {
barcodeLoading.style.display = 'none';
barcodeError.style.display = 'none';
barcodePreview.style.display = 'block';
}
function showError(message) {
barcodeLoading.style.display = 'none';
barcodePreview.style.display = 'none';
barcodeErrorMessage.textContent = message;
barcodeError.style.display = 'block';
}
})();
</script>
}

View File

@ -1,350 +0,0 @@
@{
ViewData["Title"] = ViewBag.PageTitle;
Layout = "_Layout";
}
<div class="container-fluid">
<div class="row">
<div class="col-12">
<div class="text-center mb-4">
<h1 class="display-5">@ViewBag.PageTitle</h1>
<p class="lead text-muted">@ViewBag.PageDescription</p>
</div>
</div>
</div>
<div class="row justify-content-center">
<div class="col-lg-10 col-xl-8">
<div class="card shadow-sm">
<div class="card-header bg-primary text-white">
<h5 class="mb-0">
<i class="bi bi-file-earmark-text me-2"></i>@ViewBag.DocumentMenuTitle
</h5>
</div>
<div class="card-body">
@if (ViewBag.ConversionError != null)
{
<div class="alert alert-danger" role="alert">
<i class="bi bi-exclamation-triangle-fill me-2"></i>
@ViewBag.ConversionError
</div>
}
<ul class="nav nav-tabs mb-4" id="pdfTextTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="plain-text-tab" data-bs-toggle="tab" data-bs-target="#plain-text" type="button" role="tab" aria-controls="plain-text" aria-selected="true">
<i class="bi bi-file-text me-2"></i>@ViewBag.PdfPlainTextTabTitle
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="markdown-tab" data-bs-toggle="tab" data-bs-target="#markdown" type="button" role="tab" aria-controls="markdown" aria-selected="false">
<i class="bi bi-markdown me-2"></i>@ViewBag.PdfMarkdownTabTitle
</button>
</li>
</ul>
<div class="tab-content" id="pdfTextTabContent">
<div class="tab-pane fade show active" id="plain-text" role="tabpanel" aria-labelledby="plain-text-tab">
<form id="plainTextForm" action="/pt-BR/DocumentConverters/PdfToText/ExtractPlainText" method="post" enctype="multipart/form-data" class="needs-validation" novalidate>
<div asp-validation-summary="ModelOnly" class="alert alert-danger" role="alert"></div>
<div class="mb-4">
<label for="plainTextPdfFile" class="form-label fw-semibold">@ViewBag.PdfFileInputLabel</label>
<input class="form-control" type="file" id="plainTextPdfFile" name="pdfFile" accept=".pdf" required>
<div class="invalid-feedback">
@ViewBag.SelectFileError
</div>
</div>
<div class="mb-4">
<label for="plainTextPassword" class="form-label fw-semibold">@ViewBag.PdfPasswordLabel</label>
<input class="form-control" type="password" id="plainTextPassword" name="password" placeholder="@ViewBag.PdfPasswordPlaceholder">
<div class="form-text">
<i class="bi bi-shield-lock me-1"></i>@ViewBag.PdfPasswordHint
</div>
</div>
<div class="d-grid gap-2">
<button type="button" id="previewPlainTextBtn" class="btn btn-primary">
<i class="bi bi-eye me-2"></i>@ViewBag.ExtractPlainTextButton
</button>
<button type="submit" class="btn btn-outline-secondary">
<i class="bi bi-download me-2"></i>@ViewBag.DownloadPlainTextButton
</button>
</div>
</form>
</div>
<div class="tab-pane fade" id="markdown" role="tabpanel" aria-labelledby="markdown-tab">
<form id="markdownForm" action="/pt-BR/DocumentConverters/PdfToText/ExtractMarkdown" method="post" enctype="multipart/form-data" class="needs-validation" novalidate>
<div asp-validation-summary="ModelOnly" class="alert alert-danger" role="alert"></div>
<div class="mb-4">
<label for="markdownPdfFile" class="form-label fw-semibold">@ViewBag.PdfFileInputLabel</label>
<input class="form-control" type="file" id="markdownPdfFile" name="pdfFile" accept=".pdf" required>
<div class="invalid-feedback">
@ViewBag.SelectFileError
</div>
</div>
<div class="mb-4">
<label for="markdownPassword" class="form-label fw-semibold">@ViewBag.PdfPasswordLabel</label>
<input class="form-control" type="password" id="markdownPassword" name="password" placeholder="@ViewBag.PdfPasswordPlaceholder">
<div class="form-text">
<i class="bi bi-shield-lock me-1"></i>@ViewBag.PdfPasswordHint
</div>
</div>
<div class="d-grid gap-2">
<button type="button" id="previewMarkdownBtn" class="btn btn-primary">
<i class="bi bi-eye me-2"></i>@ViewBag.ExtractMarkdownButton
</button>
<button type="submit" class="btn btn-outline-secondary">
<i class="bi bi-download me-2"></i>@ViewBag.DownloadMarkdownButton
</button>
</div>
</form>
</div>
</div>
<div id="loadingSpinner" class="text-center my-4" style="display: none;">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Carregando...</span>
</div>
</div>
<div id="previewArea" class="mt-4" style="display: none;">
<h5 class="mb-3">@ViewBag.PdfTextPreviewTitle</h5>
<textarea id="previewTextArea" class="form-control" rows="12" readonly></textarea>
<div class="text-end mt-3">
<button id="downloadPreviewBtn" class="btn btn-success btn-sm">
<i class="bi bi-box-arrow-down me-2"></i>@ViewBag.DownloadPlainTextButton
</button>
</div>
</div>
<div id="errorArea" class="mt-4" style="display: none;">
<div class="alert alert-warning" role="alert">
<i class="bi bi-exclamation-triangle me-2"></i>
<span id="errorMessage"></span>
<button id="fallbackDownloadBtn" class="btn btn-outline-primary btn-sm ms-2">
@ViewBag.DownloadPlainTextButton
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row mt-5">
<div class="col-lg-10 mx-auto">
<div class="converter-faq">
<h3 class="h4 mb-3 text-center">Perguntas Frequentes</h3>
<div class="accordion" id="pdfTextFaqAccordion">
<div class="accordion-item">
<h2 class="accordion-header" id="faqWhat">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseWhat" aria-expanded="false" aria-controls="collapseWhat">
<i class="bi bi-question-circle me-2"></i>@ViewBag.FaqWhatTitle
</button>
</h2>
<div id="collapseWhat" class="accordion-collapse collapse" aria-labelledby="faqWhat" data-bs-parent="#pdfTextFaqAccordion">
<div class="accordion-body">
@ViewBag.FaqWhatContent
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" id="faqHow">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseHow" aria-expanded="false" aria-controls="collapseHow">
<i class="bi bi-gear me-2"></i>@ViewBag.FaqHowTitle
</button>
</h2>
<div id="collapseHow" class="accordion-collapse collapse" aria-labelledby="faqHow" data-bs-parent="#pdfTextFaqAccordion">
<div class="accordion-body">
@ViewBag.FaqHowContent
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" id="faqWhy">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseWhy" aria-expanded="false" aria-controls="collapseWhy">
<i class="bi bi-lightbulb me-2"></i>@ViewBag.FaqWhyTitle
</button>
</h2>
<div id="collapseWhy" class="accordion-collapse collapse" aria-labelledby="faqWhy" data-bs-parent="#pdfTextFaqAccordion">
<div class="accordion-body">
@ViewBag.FaqWhyContent
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" id="faqSecurity">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseSecurity" aria-expanded="false" aria-controls="collapseSecurity">
<i class="bi bi-shield-check me-2"></i>@ViewBag.FaqSecurityTitle
</button>
</h2>
<div id="collapseSecurity" class="accordion-collapse collapse" aria-labelledby="faqSecurity" data-bs-parent="#pdfTextFaqAccordion">
<div class="accordion-body">
@ViewBag.FaqSecurityContent
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" id="faqLimits">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseLimits" aria-expanded="false" aria-controls="collapseLimits">
<i class="bi bi-exclamation-triangle me-2"></i>@ViewBag.FaqLimitsTitle
</button>
</h2>
<div id="collapseLimits" class="accordion-collapse collapse" aria-labelledby="faqLimits" data-bs-parent="#pdfTextFaqAccordion">
<div class="accordion-body">
@ViewBag.FaqLimitsContent
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<style>
#previewTextArea {
font-family: monospace;
background-color: #f8f9fa;
}
.spinner-border {
width: 3rem;
height: 3rem;
}
</style>
@section Scripts {
<script>
(function () {
const plainForm = document.getElementById('plainTextForm');
const markdownForm = document.getElementById('markdownForm');
const previewPlainBtn = document.getElementById('previewPlainTextBtn');
const previewMarkdownBtn = document.getElementById('previewMarkdownBtn');
const downloadPreviewBtn = document.getElementById('downloadPreviewBtn');
const fallbackDownloadBtn = document.getElementById('fallbackDownloadBtn');
const loadingSpinner = document.getElementById('loadingSpinner');
const previewArea = document.getElementById('previewArea');
const errorArea = document.getElementById('errorArea');
const previewTextArea = document.getElementById('previewTextArea');
const errorMessage = document.getElementById('errorMessage');
const previewTitleButton = document.getElementById('downloadPreviewBtn');
let previewContent = '';
let previewFileName = 'resultado.txt';
let previewMime = 'text/plain';
const forms = document.querySelectorAll('.needs-validation');
Array.from(forms).forEach(function (form) {
form.addEventListener('submit', function (event) {
if (!form.checkValidity()) {
event.preventDefault();
event.stopPropagation();
}
form.classList.add('was-validated');
});
});
previewPlainBtn.addEventListener('click', function () {
handlePreview(plainForm, 'text/plain');
});
previewMarkdownBtn.addEventListener('click', function () {
handlePreview(markdownForm, 'text/markdown');
});
downloadPreviewBtn.addEventListener('click', function () {
if (!previewContent) {
return;
}
const blob = new Blob([previewContent], { type: previewMime + ';charset=utf-8' });
const url = window.URL.createObjectURL(blob);
const anchor = document.createElement('a');
anchor.href = url;
anchor.download = previewFileName;
document.body.appendChild(anchor);
anchor.click();
document.body.removeChild(anchor);
window.URL.revokeObjectURL(url);
});
fallbackDownloadBtn.addEventListener('click', function () {
const activeTab = document.querySelector('.nav-link.active');
const currentForm = activeTab.id === 'plain-text-tab' ? plainForm : markdownForm;
currentForm.submit();
});
function handlePreview(form, mimeType) {
if (!form.checkValidity()) {
form.classList.add('was-validated');
return;
}
showLoading();
const formData = new FormData(form);
const requestUrl = form.action + '?preview=true';
console.log('DEBUG: Requesting URL:', requestUrl);
console.log('DEBUG: Form action:', form.action);
fetch(requestUrl, {
method: 'POST',
body: formData
}).then(async response => {
console.log('DEBUG: Response status:', response.status);
console.log('DEBUG: Response headers:', response.headers.get('content-type'));
const responseText = await response.text();
console.log('DEBUG: Response text (first 200 chars):', responseText.substring(0, 200));
if (!response.ok) {
throw new Error(responseText || response.statusText);
}
const result = JSON.parse(responseText);
if (!result.success) {
throw new Error(result.message || 'Erro na conversão');
}
previewContent = result.content;
previewFileName = result.filename;
previewMime = mimeType;
previewTextArea.value = result.content;
previewTitleButton.innerHTML = `<i class="bi bi-box-arrow-down me-2"></i>${result.format === 'markdown' ? '@ViewBag.DownloadMarkdownButton' : '@ViewBag.DownloadPlainTextButton'}`;
showPreview();
}).catch(error => {
showError(error.message || 'Erro inesperado.');
});
}
function showLoading() {
loadingSpinner.style.display = 'block';
previewArea.style.display = 'none';
errorArea.style.display = 'none';
}
function showPreview() {
loadingSpinner.style.display = 'none';
errorArea.style.display = 'none';
previewArea.style.display = 'block';
}
function showError(message) {
loadingSpinner.style.display = 'none';
previewArea.style.display = 'none';
errorMessage.textContent = message;
errorArea.style.display = 'block';
}
})();
</script>
}

View File

@ -27,9 +27,6 @@ namespace Convert_It_Online.Areas.ImageConverters.Controllers
ViewBag.CaseConverterTitle = _localizer["CaseConverterTitle"]; ViewBag.CaseConverterTitle = _localizer["CaseConverterTitle"];
ViewBag.JpgToWebpTitle = _localizer["JpgToWebpTitle"]; ViewBag.JpgToWebpTitle = _localizer["JpgToWebpTitle"];
ViewBag.HeicToJpgTitle = _localizer["HeicToJpgTitle"]; ViewBag.HeicToJpgTitle = _localizer["HeicToJpgTitle"];
ViewBag.DocumentMenuTitle = _localizer["DocumentMenuTitle"];
ViewBag.PdfToTextTitle = _localizer["PdfToTextTitle"];
ViewBag.PdfBarcodeTitle = _localizer["PdfBarcodeTitle"];
ViewBag.FooterText = _localizer["FooterText"]; ViewBag.FooterText = _localizer["FooterText"];
ViewBag.About = _localizer["About"]; ViewBag.About = _localizer["About"];
ViewBag.Contact = _localizer["Contact"]; ViewBag.Contact = _localizer["Contact"];
@ -59,7 +56,6 @@ namespace Convert_It_Online.Areas.ImageConverters.Controllers
ViewBag.FaqSecurityContent = _localizer["HeicFaqSecurityContent"]; ViewBag.FaqSecurityContent = _localizer["HeicFaqSecurityContent"];
ViewBag.FaqLimitsTitle = _localizer["HeicFaqLimitsTitle"]; ViewBag.FaqLimitsTitle = _localizer["HeicFaqLimitsTitle"];
ViewBag.FaqLimitsContent = _localizer["HeicFaqLimitsContent"]; ViewBag.FaqLimitsContent = _localizer["HeicFaqLimitsContent"];
ViewBag.MetaDescription = ViewBag.PageDescription;
} }
public IActionResult Index() public IActionResult Index()

View File

@ -25,10 +25,6 @@ namespace Convert_It_Online.Areas.ImageConverters.Controllers
ViewBag.ImageMenuTitle = _localizer["ImageMenuTitle"]; ViewBag.ImageMenuTitle = _localizer["ImageMenuTitle"];
ViewBag.CaseConverterTitle = _localizer["CaseConverterTitle"]; ViewBag.CaseConverterTitle = _localizer["CaseConverterTitle"];
ViewBag.JpgToWebpTitle = _localizer["JpgToWebpTitle"]; ViewBag.JpgToWebpTitle = _localizer["JpgToWebpTitle"];
ViewBag.HeicToJpgTitle = _localizer["HeicToJpgTitle"];
ViewBag.DocumentMenuTitle = _localizer["DocumentMenuTitle"];
ViewBag.PdfToTextTitle = _localizer["PdfToTextTitle"];
ViewBag.PdfBarcodeTitle = _localizer["PdfBarcodeTitle"];
ViewBag.FooterText = _localizer["FooterText"]; ViewBag.FooterText = _localizer["FooterText"];
ViewBag.About = _localizer["About"]; ViewBag.About = _localizer["About"];
ViewBag.Contact = _localizer["Contact"]; ViewBag.Contact = _localizer["Contact"];
@ -58,7 +54,6 @@ namespace Convert_It_Online.Areas.ImageConverters.Controllers
ViewBag.FaqSecurityContent = _localizer["JpgWebpFaqSecurityContent"]; ViewBag.FaqSecurityContent = _localizer["JpgWebpFaqSecurityContent"];
ViewBag.FaqLimitsTitle = _localizer["JpgWebpFaqLimitsTitle"]; ViewBag.FaqLimitsTitle = _localizer["JpgWebpFaqLimitsTitle"];
ViewBag.FaqLimitsContent = _localizer["JpgWebpFaqLimitsContent"]; ViewBag.FaqLimitsContent = _localizer["JpgWebpFaqLimitsContent"];
ViewBag.MetaDescription = ViewBag.PageDescription;
} }
public IActionResult Index() public IActionResult Index()

View File

@ -37,10 +37,6 @@ namespace Convert_It_Online.Areas.TextTools.Controllers
ViewBag.ImageMenuTitle = _localizer["ImageMenuTitle"]; ViewBag.ImageMenuTitle = _localizer["ImageMenuTitle"];
ViewBag.CaseConverterTitle = _localizer["CaseConverterTitle"]; ViewBag.CaseConverterTitle = _localizer["CaseConverterTitle"];
ViewBag.JpgToWebpTitle = _localizer["JpgToWebpTitle"]; ViewBag.JpgToWebpTitle = _localizer["JpgToWebpTitle"];
ViewBag.HeicToJpgTitle = _localizer["HeicToJpgTitle"];
ViewBag.DocumentMenuTitle = _localizer["DocumentMenuTitle"];
ViewBag.PdfToTextTitle = _localizer["PdfToTextTitle"];
ViewBag.PdfBarcodeTitle = _localizer["PdfBarcodeTitle"];
ViewBag.FooterText = _localizer["FooterText"]; ViewBag.FooterText = _localizer["FooterText"];
ViewBag.About = _localizer["About"]; ViewBag.About = _localizer["About"];
ViewBag.Contact = _localizer["Contact"]; ViewBag.Contact = _localizer["Contact"];
@ -70,7 +66,6 @@ namespace Convert_It_Online.Areas.TextTools.Controllers
ViewBag.FaqSecurityContent = _localizer["CaseFaqSecurityContent"]; ViewBag.FaqSecurityContent = _localizer["CaseFaqSecurityContent"];
ViewBag.FaqLimitsTitle = _localizer["CaseFaqLimitsTitle"]; ViewBag.FaqLimitsTitle = _localizer["CaseFaqLimitsTitle"];
ViewBag.FaqLimitsContent = _localizer["CaseFaqLimitsContent"]; ViewBag.FaqLimitsContent = _localizer["CaseFaqLimitsContent"];
ViewBag.MetaDescription = ViewBag.PageTitle;
var model = new CaseConverterViewModel(); var model = new CaseConverterViewModel();
return View(model); return View(model);
@ -88,7 +83,6 @@ namespace Convert_It_Online.Areas.TextTools.Controllers
ViewBag.ToLowerButton = _localizer["ToLowerButton"]; ViewBag.ToLowerButton = _localizer["ToLowerButton"];
ViewBag.ToSentenceCaseButton = _localizer["ToSentenceCaseButton"]; ViewBag.ToSentenceCaseButton = _localizer["ToSentenceCaseButton"];
ViewBag.ResultTitle = _localizer["ResultTitle"]; ViewBag.ResultTitle = _localizer["ResultTitle"];
ViewBag.MetaDescription = ViewBag.PageTitle;
if (!ModelState.IsValid || string.IsNullOrEmpty(model.InputText)) if (!ModelState.IsValid || string.IsNullOrEmpty(model.InputText))
{ {

View File

@ -13,12 +13,10 @@ Convert-It Online é uma aplicação web ASP.NET Core que oferece ferramentas gr
**Português (pt-BR):** **Português (pt-BR):**
- Ferramentas de Texto: `/pt-BR/ferramentas-de-texto/conversor-de-maiusculas-minusculas` - Ferramentas de Texto: `/pt-BR/ferramentas-de-texto/conversor-de-maiusculas-minusculas`
- Conversores de Imagem: `/pt-BR/conversores-de-imagem/jpg-para-webp` - Conversores de Imagem: `/pt-BR/conversores-de-imagem/jpg-para-webp`
- Conversores de Documento: `/pt-BR/conversores-de-documento/pdf-para-texto`
**Espanhol (es-MX, es-CL, es-PY):** **Espanhol (es-MX, es-CL, es-PY):**
- Herramientas de Texto: `/es-MX/herramientas-de-texto/conversor-de-mayusculas-minusculas` - Herramientas de Texto: `/es-MX/herramientas-de-texto/conversor-de-mayusculas-minusculas`
- Convertidores de Imagen: `/es-MX/convertidores-de-imagen/jpg-a-webp` - Convertidores de Imagen: `/es-MX/convertidores-de-imagen/jpg-a-webp`
- Convertidores de Documento: `/es-MX/convertidores-de-documento/pdf-a-texto`
### Arquitetura do Sistema de URLs ### Arquitetura do Sistema de URLs
@ -55,12 +53,6 @@ Convert-It Online é uma aplicação web ASP.NET Core que oferece ferramentas gr
- **URL PT**: `/pt-BR/conversores-de-imagem/jpg-para-webp` - **URL PT**: `/pt-BR/conversores-de-imagem/jpg-para-webp`
- **URL ES**: `/es-MX/convertidores-de-imagen/jpg-a-webp` - **URL ES**: `/es-MX/convertidores-de-imagen/jpg-a-webp`
### DocumentConverters
- **Controllers**: `PdfToTextController`, `PdfBarcodeLineController`
- **Funcionalidades**: Extrair texto/Markdown de PDFs e detectar linha digitável de boletos
- **URL PT**: `/pt-BR/conversores-de-documento/pdf-para-texto` e `/pt-BR/conversores-de-documento/linha-digitavel-do-pdf`
- **URL ES**: `/es-MX/convertidores-de-documento/pdf-a-texto` e `/es-MX/convertidores-de-documento/linea-digitada-desde-pdf`
## Desenvolvimento Guidelines ## Desenvolvimento Guidelines
### Adicionando Novos Conversores ### Adicionando Novos Conversores

View File

@ -32,9 +32,6 @@ namespace Convert_It_Online.Controllers
ViewBag.CaseConverterTitle = _localizer["CaseConverterTitle"]; ViewBag.CaseConverterTitle = _localizer["CaseConverterTitle"];
ViewBag.JpgToWebpTitle = _localizer["JpgToWebpTitle"]; ViewBag.JpgToWebpTitle = _localizer["JpgToWebpTitle"];
ViewBag.HeicToJpgTitle = _localizer["HeicToJpgTitle"]; ViewBag.HeicToJpgTitle = _localizer["HeicToJpgTitle"];
ViewBag.DocumentMenuTitle = _localizer["DocumentMenuTitle"];
ViewBag.PdfToTextTitle = _localizer["PdfToTextTitle"];
ViewBag.PdfBarcodeTitle = _localizer["PdfBarcodeTitle"];
ViewBag.FooterText = _localizer["FooterText"]; ViewBag.FooterText = _localizer["FooterText"];
ViewBag.About = _localizer["About"]; ViewBag.About = _localizer["About"];
ViewBag.Contact = _localizer["Contact"]; ViewBag.Contact = _localizer["Contact"];
@ -54,7 +51,6 @@ namespace Convert_It_Online.Controllers
ViewBag.SecurityContent = _localizer["SecurityContent"]; ViewBag.SecurityContent = _localizer["SecurityContent"];
ViewBag.AllConvertersTitle = _localizer["AllConvertersTitle"]; ViewBag.AllConvertersTitle = _localizer["AllConvertersTitle"];
ViewBag.UseConverterButton = _localizer["UseConverterButton"]; ViewBag.UseConverterButton = _localizer["UseConverterButton"];
ViewBag.MetaDescription = ViewBag.Subtitle;
var individualConverters = new List<ToolViewModel> var individualConverters = new List<ToolViewModel>
{ {
@ -81,22 +77,6 @@ namespace Convert_It_Online.Controllers
Area = "ImageConverters", Area = "ImageConverters",
Controller = "HeicToJpg", Controller = "HeicToJpg",
IconClass = "bi-phone" IconClass = "bi-phone"
},
new ToolViewModel
{
Title = _localizer["PdfToTextIndividualTitle"],
Description = _localizer["PdfToTextIndividualDescription"],
Area = "DocumentConverters",
Controller = "PdfToText",
IconClass = "bi-file-text"
},
new ToolViewModel
{
Title = _localizer["PdfBarcodeIndividualTitle"],
Description = _localizer["PdfBarcodeIndividualDescription"],
Area = "DocumentConverters",
Controller = "PdfBarcodeLine",
IconClass = "bi-upc-scan"
} }
}; };
@ -117,7 +97,6 @@ namespace Convert_It_Online.Controllers
ViewBag.Feature3 = _localizer["Feature3"]; ViewBag.Feature3 = _localizer["Feature3"];
ViewBag.Feature4 = _localizer["Feature4"]; ViewBag.Feature4 = _localizer["Feature4"];
ViewBag.StartConverting = _localizer["StartConverting"]; ViewBag.StartConverting = _localizer["StartConverting"];
ViewBag.MetaDescription = ViewBag.WhatIsContent;
return View(); return View();
} }
@ -130,7 +109,6 @@ namespace Convert_It_Online.Controllers
ViewBag.SupportTitle = _localizer["SupportTitle"]; ViewBag.SupportTitle = _localizer["SupportTitle"];
ViewBag.SupportTime = _localizer["SupportTime"]; ViewBag.SupportTime = _localizer["SupportTime"];
ViewBag.FaqTitle = _localizer["FaqTitle"]; ViewBag.FaqTitle = _localizer["FaqTitle"];
ViewBag.MetaDescription = ViewBag.ContactIntro;
return View(); return View();
} }
@ -151,7 +129,6 @@ namespace Convert_It_Online.Controllers
ViewBag.ChangesContent = _localizer["ChangesContent"]; ViewBag.ChangesContent = _localizer["ChangesContent"];
ViewBag.LastUpdated = _localizer["LastUpdated"]; ViewBag.LastUpdated = _localizer["LastUpdated"];
ViewBag.BackToHome = _localizer["BackToHome"]; ViewBag.BackToHome = _localizer["BackToHome"];
ViewBag.MetaDescription = ViewBag.TermsIntro;
return View(); return View();
} }
} }

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk.Web"> <Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
@ -11,7 +11,6 @@
<PackageReference Include="HeyRed.ImageSharp.Heif" Version="2.1.3" /> <PackageReference Include="HeyRed.ImageSharp.Heif" Version="2.1.3" />
<PackageReference Include="LibHeif.Native.win-x64" Version="1.15.1" /> <PackageReference Include="LibHeif.Native.win-x64" Version="1.15.1" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.7" /> <PackageReference Include="SixLabors.ImageSharp" Version="3.1.7" />
<PackageReference Include="itext7" Version="8.0.5" />
<PackageReference Include="Serilog" Version="4.1.0" /> <PackageReference Include="Serilog" Version="4.1.0" />
<PackageReference Include="Serilog.AspNetCore" Version="8.0.3" /> <PackageReference Include="Serilog.AspNetCore" Version="8.0.3" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="8.0.0" /> <PackageReference Include="Serilog.Extensions.Hosting" Version="8.0.0" />
@ -23,9 +22,6 @@
<PackageReference Include="Serilog.Enrichers.Environment" Version="3.0.1" /> <PackageReference Include="Serilog.Enrichers.Environment" Version="3.0.1" />
<PackageReference Include="Serilog.Enrichers.Process" Version="3.0.0" /> <PackageReference Include="Serilog.Enrichers.Process" Version="3.0.0" />
<PackageReference Include="Serilog.Enrichers.Thread" Version="4.0.0" /> <PackageReference Include="Serilog.Enrichers.Thread" Version="4.0.0" />
<PackageReference Include="Whisper.net" Version="1.9.0" />
<PackageReference Include="Whisper.net.Runtime" Version="1.9.0" />
<PackageReference Include="Xabe.FFmpeg" Version="6.0.2" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -44,14 +44,6 @@ FROM base AS final
WORKDIR /app WORKDIR /app
COPY --from=publish /app/publish . COPY --from=publish /app/publish .
# Instalar ffmpeg e bibliotecas nativas (rodar como root)
USER root
RUN apt-get update && apt-get install -y \
ffmpeg \
libc6-dev \
&& rm -rf /var/lib/apt/lists/*
USER app
# Variáveis de ambiente otimizadas para produção # Variáveis de ambiente otimizadas para produção
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false
ENV DOTNET_USE_POLLING_FILE_WATCHER=true ENV DOTNET_USE_POLLING_FILE_WATCHER=true

View File

@ -35,14 +35,6 @@ namespace Convert_It_Online.Middleware
var translatedController = match.Groups[3].Value; var translatedController = match.Groups[3].Value;
var action = match.Groups[4].Success ? match.Groups[4].Value : "Index"; var action = match.Groups[4].Success ? match.Groups[4].Value : "Index";
// Skip if this is already an internal area/controller name (starts with capital letter)
if (char.IsUpper(translatedArea[0]) || char.IsUpper(translatedController[0]))
{
_logger.LogInformation($"Skipping internal path: {path}");
await _next(context);
return;
}
_logger.LogInformation($"Matched: culture={cultureName}, area={translatedArea}, controller={translatedController}, action={action}"); _logger.LogInformation($"Matched: culture={cultureName}, area={translatedArea}, controller={translatedController}, action={action}");
try try

View File

@ -163,7 +163,6 @@ builder.Host.UseSerilog();
builder.Services.AddLocalization(); builder.Services.AddLocalization();
builder.Services.AddSingleton<IUrlTranslationService, UrlTranslationService>(); builder.Services.AddSingleton<IUrlTranslationService, UrlTranslationService>();
builder.Services.AddSingleton<IAudioTranscriptionService, AudioTranscriptionService>();
var supportedCultures = new[] { "pt-BR", "es-MX", "es-CL", "es-PY" }; var supportedCultures = new[] { "pt-BR", "es-MX", "es-CL", "es-PY" };
builder.Services.Configure<RequestLocalizationOptions>(options => builder.Services.Configure<RequestLocalizationOptions>(options =>

View File

@ -1,47 +1 @@
# Convert-It Online 
Ferramenta multiuso de conversão de arquivos (Imagens, Documentos, Texto e Áudio) desenvolvida em ASP.NET Core 8 MVC.
## 🛠️ Funcionalidades
- **Imagens:** HEIC para JPG, JPG para WebP.
- **Documentos:** PDF para Texto, Extração de Linha Digitável de Boletos (Barcode).
- **Texto:** Conversor de Case (Maiúsculo/Minúsculo).
- **Áudio:** Transcrição de Áudio para Texto (Whisper AI) e Texto para Voz (Web Speech API).
- **PWA:** Suporte a instalação e integração com menu de compartilhamento do Android (Share Target).
## 🚀 Dependências Externas (Obrigatório)
Para as funcionalidades de áudio (transcrição), o projeto depende do **FFmpeg**.
### 🐧 Linux (Ubuntu/Debian)
```bash
sudo apt update
sudo apt install ffmpeg
```
### 🪟 Windows
1. Baixe os binários em [ffmpeg.org](https://ffmpeg.org/download.html).
2. Extraia para uma pasta (ex: `C:\ffmpeg`).
3. Adicione a pasta `bin` (ex: `C:\ffmpeg\bin`) às **Variáveis de Ambiente do Sistema (PATH)**.
4. Reinicie o terminal ou o Visual Studio.
### 🐳 Docker
A imagem Docker já está configurada para instalar o `ffmpeg` automaticamente durante o build.
## 💻 Desenvolvimento Local
1. Certifique-se de ter o .NET 8 SDK instalado.
2. Clone o repositório.
3. Configure o FFmpeg conforme instruções acima.
4. Execute o comando:
```bash
dotnet run
```
## 📱 PWA & Android Share Target
O projeto está configurado como um Progressive Web App. Ao "Instalar" o site no Android:
1. Ele aparecerá como um aplicativo nativo.
2. Você poderá compartilhar arquivos de áudio diretamente do WhatsApp para o Convert-It para transcrição automática.
---
Desenvolvido por Ricardo.

View File

@ -1,97 +0,0 @@
using System;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
using Whisper.net;
using Whisper.net.Ggml;
using Xabe.FFmpeg;
using Microsoft.Extensions.Logging;
namespace Convert_It_Online.Services
{
public class AudioTranscriptionService : IAudioTranscriptionService
{
private readonly string _modelPath;
private readonly ILogger<AudioTranscriptionService> _logger;
private readonly HttpClient _httpClient;
public AudioTranscriptionService(ILogger<AudioTranscriptionService> logger)
{
_logger = logger;
_httpClient = new HttpClient();
_modelPath = Path.Combine(AppContext.BaseDirectory, "Models", "ggml-base.bin");
// Garantir que a pasta Models existe
var modelsDir = Path.GetDirectoryName(_modelPath);
if (!Directory.Exists(modelsDir))
{
Directory.CreateDirectory(modelsDir!);
}
}
private async Task EnsureModelExistsAsync()
{
if (!System.IO.File.Exists(_modelPath))
{
_logger.LogInformation("Baixando modelo Whisper Base...");
var downloader = new WhisperGgmlDownloader(_httpClient);
using var modelStream = await downloader.GetGgmlModelAsync(GgmlType.Base);
using var fileStream = System.IO.File.Create(_modelPath);
await modelStream.CopyToAsync(fileStream);
_logger.LogInformation("Modelo Whisper baixado com sucesso.");
}
}
public async Task<string> TranscribeAsync(string inputPath, string culture = "pt-BR")
{
await EnsureModelExistsAsync();
string tempWavPath = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}.wav");
try
{
_logger.LogInformation("Convertendo áudio para WAV 16kHz Mono...");
// Configurar FFmpeg (assume que está no PATH em Linux)
// Se estiver no Windows, pode precisar de FFmpeg.SetExecutablesPath
var conversion = await FFmpeg.Conversions.New()
.AddParameter($"-i \"{inputPath}\"")
.AddParameter("-ar 16000")
.AddParameter("-ac 1")
.AddParameter("-c:a pcm_s16le")
.SetOutput(tempWavPath)
.Start();
_logger.LogInformation("Iniciando transcrição com Whisper...");
using var factory = WhisperFactory.FromPath(_modelPath);
using var processor = factory.CreateBuilder()
.WithLanguage(culture.Split('-')[0]) // Usa "pt", "es", etc
.Build();
using var wavStream = System.IO.File.OpenRead(tempWavPath);
var result = "";
await foreach (var segment in processor.ProcessAsync(wavStream))
{
result += segment.Text + " ";
}
return result.Trim();
}
catch (Exception ex)
{
_logger.LogError(ex, "Erro durante a transcrição de áudio.");
throw;
}
finally
{
if (System.IO.File.Exists(tempWavPath))
{
System.IO.File.Delete(tempWavPath);
}
}
}
}
}

View File

@ -1,9 +0,0 @@
using System.Threading.Tasks;
namespace Convert_It_Online.Services
{
public interface IAudioTranscriptionService
{
Task<string> TranscribeAsync(string inputPath, string culture = "pt-BR");
}
}

View File

@ -16,26 +16,22 @@ namespace Convert_It_Online.Services
["pt-BR"] = new Dictionary<string, string> ["pt-BR"] = new Dictionary<string, string>
{ {
["TextTools"] = "ferramentas-de-texto", ["TextTools"] = "ferramentas-de-texto",
["ImageConverters"] = "conversores-de-imagem", ["ImageConverters"] = "conversores-de-imagem"
["DocumentConverters"] = "conversores-de-documento"
}, },
["es-MX"] = new Dictionary<string, string> ["es-MX"] = new Dictionary<string, string>
{ {
["TextTools"] = "herramientas-de-texto", ["TextTools"] = "herramientas-de-texto",
["ImageConverters"] = "convertidores-de-imagen", ["ImageConverters"] = "convertidores-de-imagen"
["DocumentConverters"] = "convertidores-de-documento"
}, },
["es-CL"] = new Dictionary<string, string> ["es-CL"] = new Dictionary<string, string>
{ {
["TextTools"] = "herramientas-de-texto", ["TextTools"] = "herramientas-de-texto",
["ImageConverters"] = "convertidores-de-imagen", ["ImageConverters"] = "convertidores-de-imagen"
["DocumentConverters"] = "convertidores-de-documento"
}, },
["es-PY"] = new Dictionary<string, string> ["es-PY"] = new Dictionary<string, string>
{ {
["TextTools"] = "herramientas-de-texto", ["TextTools"] = "herramientas-de-texto",
["ImageConverters"] = "convertidores-de-imagen", ["ImageConverters"] = "convertidores-de-imagen"
["DocumentConverters"] = "convertidores-de-documento"
} }
}; };
@ -45,33 +41,25 @@ namespace Convert_It_Online.Services
{ {
["CaseConverter"] = "conversor-de-maiusculas-minusculas", ["CaseConverter"] = "conversor-de-maiusculas-minusculas",
["JpgToWebp"] = "jpg-para-webp", ["JpgToWebp"] = "jpg-para-webp",
["HeicToJpg"] = "heic-para-jpg", ["HeicToJpg"] = "heic-para-jpg"
["PdfToText"] = "pdf-para-texto",
["PdfBarcodeLine"] = "linha-digitavel-do-pdf"
}, },
["es-MX"] = new Dictionary<string, string> ["es-MX"] = new Dictionary<string, string>
{ {
["CaseConverter"] = "conversor-de-mayusculas-minusculas", ["CaseConverter"] = "conversor-de-mayusculas-minusculas",
["JpgToWebp"] = "jpg-a-webp", ["JpgToWebp"] = "jpg-a-webp",
["HeicToJpg"] = "heic-a-jpg", ["HeicToJpg"] = "heic-a-jpg"
["PdfToText"] = "pdf-a-texto",
["PdfBarcodeLine"] = "linea-digitada-desde-pdf"
}, },
["es-CL"] = new Dictionary<string, string> ["es-CL"] = new Dictionary<string, string>
{ {
["CaseConverter"] = "conversor-de-mayusculas-minusculas", ["CaseConverter"] = "conversor-de-mayusculas-minusculas",
["JpgToWebp"] = "jpg-a-webp", ["JpgToWebp"] = "jpg-a-webp",
["HeicToJpg"] = "heic-a-jpg", ["HeicToJpg"] = "heic-a-jpg"
["PdfToText"] = "pdf-a-texto",
["PdfBarcodeLine"] = "linea-digitada-desde-pdf"
}, },
["es-PY"] = new Dictionary<string, string> ["es-PY"] = new Dictionary<string, string>
{ {
["CaseConverter"] = "conversor-de-mayusculas-minusculas", ["CaseConverter"] = "conversor-de-mayusculas-minusculas",
["JpgToWebp"] = "jpg-a-webp", ["JpgToWebp"] = "jpg-a-webp",
["HeicToJpg"] = "heic-a-jpg", ["HeicToJpg"] = "heic-a-jpg"
["PdfToText"] = "pdf-a-texto",
["PdfBarcodeLine"] = "linea-digitada-desde-pdf"
} }
}; };

View File

@ -51,21 +51,12 @@
<data name="ImageMenuTitle" xml:space="preserve"> <data name="ImageMenuTitle" xml:space="preserve">
<value>Imagen</value> <value>Imagen</value>
</data> </data>
<data name="DocumentMenuTitle" xml:space="preserve">
<value>Documentos</value>
</data>
<data name="CaseConverterTitle" xml:space="preserve"> <data name="CaseConverterTitle" xml:space="preserve">
<value>Convertidor de Mayúsculas/Minúsculas</value> <value>Convertidor de Mayúsculas/Minúsculas</value>
</data> </data>
<data name="JpgToWebpTitle" xml:space="preserve"> <data name="JpgToWebpTitle" xml:space="preserve">
<value>JPG a WebP</value> <value>JPG a WebP</value>
</data> </data>
<data name="PdfToTextTitle" xml:space="preserve">
<value>PDF a Texto/Markdown</value>
</data>
<data name="PdfBarcodeTitle" xml:space="preserve">
<value>Detectar línea digitada</value>
</data>
<!-- Footer --> <!-- Footer -->
<data name="FooterText" xml:space="preserve"> <data name="FooterText" xml:space="preserve">
@ -122,24 +113,12 @@
<data name="UrlImageConverters" xml:space="preserve"> <data name="UrlImageConverters" xml:space="preserve">
<value>convertidores-de-imagen</value> <value>convertidores-de-imagen</value>
</data> </data>
<data name="UrlDocumentConverters" xml:space="preserve">
<value>convertidores-de-documento</value>
</data>
<data name="UrlCaseConverter" xml:space="preserve"> <data name="UrlCaseConverter" xml:space="preserve">
<value>conversor-de-mayusculas-minusculas</value> <value>conversor-de-mayusculas-minusculas</value>
</data> </data>
<data name="UrlJpgToWebp" xml:space="preserve"> <data name="UrlJpgToWebp" xml:space="preserve">
<value>jpg-a-webp</value> <value>jpg-a-webp</value>
</data> </data>
<data name="UrlHeicToJpg" xml:space="preserve">
<value>heic-a-jpg</value>
</data>
<data name="UrlPdfToText" xml:space="preserve">
<value>pdf-a-texto</value>
</data>
<data name="UrlPdfBarcode" xml:space="preserve">
<value>linea-digitada-desde-pdf</value>
</data>
<!-- New individual converter resources - Mexican Spanish --> <!-- New individual converter resources - Mexican Spanish -->
<data name="AllConvertersTitle" xml:space="preserve"> <data name="AllConvertersTitle" xml:space="preserve">
@ -163,18 +142,6 @@
<data name="HeicToJpgIndividualDescription" xml:space="preserve"> <data name="HeicToJpgIndividualDescription" xml:space="preserve">
<value>Convierte fotos HEIC del iPhone al formato JPG universalmente compatible.</value> <value>Convierte fotos HEIC del iPhone al formato JPG universalmente compatible.</value>
</data> </data>
<data name="PdfToTextIndividualTitle" xml:space="preserve">
<value>Extraer texto de PDF</value>
</data>
<data name="PdfToTextIndividualDescription" xml:space="preserve">
<value>Transforma tus PDFs en texto simple o Markdown, incluso cuando requieren contraseña.</value>
</data>
<data name="PdfBarcodeIndividualTitle" xml:space="preserve">
<value>Detectar línea digitada</value>
</data>
<data name="PdfBarcodeIndividualDescription" xml:space="preserve">
<value>Obtén la línea digitada de boletos directamente desde el PDF, con soporte para archivos protegidos.</value>
</data>
<!-- HEIC converter menu item --> <!-- HEIC converter menu item -->
<data name="HeicToJpgTitle" xml:space="preserve"> <data name="HeicToJpgTitle" xml:space="preserve">
@ -237,167 +204,16 @@
<value>No hay límites en la cantidad de conversiones. Para garantizar un buen rendimiento, recomendamos archivos de hasta 10MB. El procesamiento es más rápido en computadoras modernas.</value> <value>No hay límites en la cantidad de conversiones. Para garantizar un buen rendimiento, recomendamos archivos de hasta 10MB. El procesamiento es más rápido en computadoras modernas.</value>
</data> </data>
<!-- PDF to Text Converter Resources -->
<data name="PdfTextConverterPageTitle" xml:space="preserve">
<value>Extraer texto de PDF</value>
</data>
<data name="PdfTextConverterPageDescription" xml:space="preserve">
<value>Convierte tus PDFs en texto simple o Markdown con soporte para archivos protegidos con contraseña.</value>
</data>
<data name="PdfPlainTextTabTitle" xml:space="preserve">
<value>PDF → Texto simple</value>
</data>
<data name="PdfMarkdownTabTitle" xml:space="preserve">
<value>PDF → Markdown</value>
</data>
<data name="PdfFileInputLabel" xml:space="preserve">
<value>Seleccionar archivo PDF</value>
</data>
<data name="PdfPasswordLabel" xml:space="preserve">
<value>Contraseña del PDF</value>
</data>
<data name="PdfPasswordPlaceholder" xml:space="preserve">
<value>Ingresa la contraseña si el PDF está protegido</value>
</data>
<data name="PdfPasswordHint" xml:space="preserve">
<value>Si el PDF requiere contraseña de apertura, escríbela aquí. De lo contrario, deja el campo vacío.</value>
</data>
<data name="ExtractPlainTextButton" xml:space="preserve">
<value>Extraer y visualizar</value>
</data>
<data name="ExtractMarkdownButton" xml:space="preserve">
<value>Extraer en Markdown</value>
</data>
<data name="DownloadPlainTextButton" xml:space="preserve">
<value>Descargar texto</value>
</data>
<data name="DownloadMarkdownButton" xml:space="preserve">
<value>Descargar Markdown</value>
</data>
<data name="PdfTextPreviewTitle" xml:space="preserve">
<value>Vista previa del contenido</value>
</data>
<data name="PdfTextFaqWhatTitle" xml:space="preserve">
<value>¿Qué hace este conversor?</value>
</data>
<data name="PdfTextFaqWhatContent" xml:space="preserve">
<value>Extrae el contenido textual del PDF y lo entrega como texto simple o Markdown, preservando cortes y secciones siempre que sea posible.</value>
</data>
<data name="PdfTextFaqHowTitle" xml:space="preserve">
<value>¿Cómo usar el conversor?</value>
</data>
<data name="PdfTextFaqHowContent" xml:space="preserve">
<value>Selecciona el archivo PDF, ingresa la contraseña si es necesario y elige la pestaña deseada. Visualiza el resultado en línea o descarga el archivo.</value>
</data>
<data name="PdfTextFaqWhyTitle" xml:space="preserve">
<value>¿Por qué convertir PDF a texto o Markdown?</value>
</data>
<data name="PdfTextFaqWhyContent" xml:space="preserve">
<value>El texto simple permite ediciones rápidas, mientras que Markdown facilita reutilizar el contenido en blogs o documentación.</value>
</data>
<data name="PdfTextFaqSecurityTitle" xml:space="preserve">
<value>¿Es seguro subir mis PDFs?</value>
</data>
<data name="PdfTextFaqSecurityContent" xml:space="preserve">
<value>Sí. Procesamos los archivos siguiendo estrictas políticas de privacidad y los eliminamos tras la conversión.</value>
</data>
<data name="PdfTextFaqLimitsTitle" xml:space="preserve">
<value>¿Existen límites de tamaño?</value>
</data>
<data name="PdfTextFaqLimitsContent" xml:space="preserve">
<value>Para la vista previa recomendamos PDFs de hasta 10MB. Archivos mayores pueden descargarse directamente en texto o Markdown.</value>
</data>
<!-- PDF Barcode Converter Resources -->
<data name="PdfBarcodeConverterPageTitle" xml:space="preserve">
<value>Extraer línea digitada de PDF</value>
</data>
<data name="PdfBarcodeConverterPageDescription" xml:space="preserve">
<value>Detecta la línea digitada de boletos o facturas directamente desde el PDF, incluso si está protegido.</value>
</data>
<data name="PdfBarcodeDetectTabTitle" xml:space="preserve">
<value>PDF → Ver línea digitada</value>
</data>
<data name="PdfBarcodeDownloadTabTitle" xml:space="preserve">
<value>PDF → Descargar línea digitada</value>
</data>
<data name="PdfBarcodeFileInputLabel" xml:space="preserve">
<value>Seleccionar PDF del boleto</value>
</data>
<data name="PdfBarcodePasswordHint" xml:space="preserve">
<value>Si el PDF está protegido con contraseña, ingrésala para permitir la lectura.</value>
</data>
<data name="DetectBarcodeButton" xml:space="preserve">
<value>Detectar línea digitada</value>
</data>
<data name="DownloadBarcodeButton" xml:space="preserve">
<value>Descargar línea digitada</value>
</data>
<data name="PdfBarcodePreviewTitle" xml:space="preserve">
<value>Línea digitada detectada</value>
</data>
<data name="PdfBarcodeFaqWhatTitle" xml:space="preserve">
<value>¿Qué es la línea digitada?</value>
</data>
<data name="PdfBarcodeFaqWhatContent" xml:space="preserve">
<value>Es la secuencia numérica del código de barras del boleto, utilizada para realizar pagos en bancos y aplicaciones.</value>
</data>
<data name="PdfBarcodeFaqHowTitle" xml:space="preserve">
<value>¿Cómo funciona el conversor?</value>
</data>
<data name="PdfBarcodeFaqHowContent" xml:space="preserve">
<value>Extrae el texto del PDF, identifica patrones de boletos (47 o 48 dígitos) y muestra la línea lista para copiar o descargar.</value>
</data>
<data name="PdfBarcodeFaqWhyTitle" xml:space="preserve">
<value>¿Cuándo usar este recurso?</value>
</data>
<data name="PdfBarcodeFaqWhyContent" xml:space="preserve">
<value>Ideal para boletos sin código de barras legible o cuando necesitas copiar la línea digitada para pagar en línea.</value>
</data>
<data name="PdfBarcodeFaqSecurityTitle" xml:space="preserve">
<value>¿Mis datos están seguros?</value>
</data>
<data name="PdfBarcodeFaqSecurityContent" xml:space="preserve">
<value>Sí. El PDF se procesa solo para extraer la línea digitada y se descarta inmediatamente después.</value>
</data>
<data name="PdfBarcodeFaqLimitsTitle" xml:space="preserve">
<value>¿Existen limitaciones?</value>
</data>
<data name="PdfBarcodeFaqLimitsContent" xml:space="preserve">
<value>Para mejores resultados utiliza PDFs con texto seleccionable. Boletos en formato de imagen pueden requerir una solución OCR externa.</value>
</data>
<!-- Error Messages --> <!-- Error Messages -->
<data name="SelectFileError" xml:space="preserve"> <data name="SelectFileError" xml:space="preserve">
<value>Por favor, selecciona un archivo para convertir.</value> <value>Por favor, selecciona un archivo para convertir.</value>
</data> </data>
<data name="CopyButtonLabel" xml:space="preserve">
<value>Copiar</value>
</data>
<data name="CopyButtonSuccess" xml:space="preserve">
<value>¡Copiado!</value>
</data>
<data name="InvalidHeicFileError" xml:space="preserve"> <data name="InvalidHeicFileError" xml:space="preserve">
<value>Archivo HEIC inválido o formato no compatible. Revisa el archivo o intenta con otro. La biblioteca nativa 'libheif' podría faltar en el sistema.</value> <value>Archivo HEIC inválido o formato no compatible. Revisa el archivo o intenta con otro. La biblioteca nativa 'libheif' podría faltar en el sistema.</value>
</data> </data>
<data name="InvalidJpgFileError" xml:space="preserve"> <data name="InvalidJpgFileError" xml:space="preserve">
<value>Archivo JPG inválido o formato no compatible. Verifica que el archivo sea una imagen JPG válida.</value> <value>Archivo JPG inválido o formato no compatible. Verifica que el archivo sea una imagen JPG válida.</value>
</data> </data>
<data name="InvalidPdfFileError" xml:space="preserve">
<value>Archivo PDF inválido, dañado o con protecciones no compatibles.</value>
</data>
<data name="PdfPasswordRequired" xml:space="preserve">
<value>Este PDF está protegido. Ingresa la contraseña para continuar.</value>
</data>
<data name="PdfInvalidPassword" xml:space="preserve">
<value>La contraseña ingresada no pudo desbloquear el PDF.</value>
</data>
<data name="PdfNoBarcodeFound" xml:space="preserve">
<value>No encontramos una línea digitada en este PDF.</value>
</data>
<data name="PdfPreviewTooLarge" xml:space="preserve">
<value>El contenido es demasiado grande para la vista previa. Descarga el resultado directamente.</value>
</data>
<!-- JPG to WebP Converter Resources --> <!-- JPG to WebP Converter Resources -->
<data name="JpgWebpConverterPageTitle" xml:space="preserve"> <data name="JpgWebpConverterPageTitle" xml:space="preserve">
<value>Convertidor JPG ↔ WebP Online Gratis</value> <value>Convertidor JPG ↔ WebP Online Gratis</value>

View File

@ -51,21 +51,12 @@
<data name="ImageMenuTitle" xml:space="preserve"> <data name="ImageMenuTitle" xml:space="preserve">
<value>Imagen</value> <value>Imagen</value>
</data> </data>
<data name="DocumentMenuTitle" xml:space="preserve">
<value>Documentos</value>
</data>
<data name="CaseConverterTitle" xml:space="preserve"> <data name="CaseConverterTitle" xml:space="preserve">
<value>Convertidor de Mayúsculas/Minúsculas</value> <value>Convertidor de Mayúsculas/Minúsculas</value>
</data> </data>
<data name="JpgToWebpTitle" xml:space="preserve"> <data name="JpgToWebpTitle" xml:space="preserve">
<value>JPG a WebP</value> <value>JPG a WebP</value>
</data> </data>
<data name="PdfToTextTitle" xml:space="preserve">
<value>PDF a Texto/Markdown</value>
</data>
<data name="PdfBarcodeTitle" xml:space="preserve">
<value>Detectar línea digitada</value>
</data>
<!-- Footer --> <!-- Footer -->
<data name="FooterText" xml:space="preserve"> <data name="FooterText" xml:space="preserve">
@ -122,21 +113,12 @@
<data name="UrlImageConverters" xml:space="preserve"> <data name="UrlImageConverters" xml:space="preserve">
<value>convertidores-de-imagen</value> <value>convertidores-de-imagen</value>
</data> </data>
<data name="UrlDocumentConverters" xml:space="preserve">
<value>convertidores-de-documento</value>
</data>
<data name="UrlCaseConverter" xml:space="preserve"> <data name="UrlCaseConverter" xml:space="preserve">
<value>conversor-de-mayusculas-minusculas</value> <value>conversor-de-mayusculas-minusculas</value>
</data> </data>
<data name="UrlJpgToWebp" xml:space="preserve"> <data name="UrlJpgToWebp" xml:space="preserve">
<value>jpg-a-webp</value> <value>jpg-a-webp</value>
</data> </data>
<data name="UrlPdfToText" xml:space="preserve">
<value>pdf-a-texto</value>
</data>
<data name="UrlPdfBarcode" xml:space="preserve">
<value>linea-digitada-desde-pdf</value>
</data>
<!-- New individual converter resources - Mexican Spanish --> <!-- New individual converter resources - Mexican Spanish -->
<data name="AllConvertersTitle" xml:space="preserve"> <data name="AllConvertersTitle" xml:space="preserve">
@ -160,18 +142,6 @@
<data name="HeicToJpgIndividualDescription" xml:space="preserve"> <data name="HeicToJpgIndividualDescription" xml:space="preserve">
<value>Convierte fotos HEIC del iPhone al formato JPG universalmente compatible.</value> <value>Convierte fotos HEIC del iPhone al formato JPG universalmente compatible.</value>
</data> </data>
<data name="PdfToTextIndividualTitle" xml:space="preserve">
<value>Extraer texto de PDF</value>
</data>
<data name="PdfToTextIndividualDescription" xml:space="preserve">
<value>Transforma tus PDFs en texto simple o Markdown, incluso cuando requieren contraseña.</value>
</data>
<data name="PdfBarcodeIndividualTitle" xml:space="preserve">
<value>Detectar línea digitada</value>
</data>
<data name="PdfBarcodeIndividualDescription" xml:space="preserve">
<value>Obtén la línea digitada de boletos directamente desde el PDF, con soporte para archivos protegidos.</value>
</data>
<!-- HEIC converter menu item --> <!-- HEIC converter menu item -->
<data name="HeicToJpgTitle" xml:space="preserve"> <data name="HeicToJpgTitle" xml:space="preserve">
@ -234,167 +204,16 @@
<value>No hay límites en la cantidad de conversiones. Para garantizar un buen rendimiento, recomendamos archivos de hasta 10MB. El procesamiento es más rápido en computadoras modernas.</value> <value>No hay límites en la cantidad de conversiones. Para garantizar un buen rendimiento, recomendamos archivos de hasta 10MB. El procesamiento es más rápido en computadoras modernas.</value>
</data> </data>
<!-- PDF to Text Converter Resources -->
<data name="PdfTextConverterPageTitle" xml:space="preserve">
<value>Extraer texto de PDF</value>
</data>
<data name="PdfTextConverterPageDescription" xml:space="preserve">
<value>Convierte tus PDFs en texto simple o Markdown con soporte para archivos protegidos con contraseña.</value>
</data>
<data name="PdfPlainTextTabTitle" xml:space="preserve">
<value>PDF → Texto simple</value>
</data>
<data name="PdfMarkdownTabTitle" xml:space="preserve">
<value>PDF → Markdown</value>
</data>
<data name="PdfFileInputLabel" xml:space="preserve">
<value>Seleccionar archivo PDF</value>
</data>
<data name="PdfPasswordLabel" xml:space="preserve">
<value>Contraseña del PDF</value>
</data>
<data name="PdfPasswordPlaceholder" xml:space="preserve">
<value>Ingresa la contraseña si el PDF está protegido</value>
</data>
<data name="PdfPasswordHint" xml:space="preserve">
<value>Si el PDF requiere contraseña de apertura, escríbela aquí. De lo contrario, deja el campo vacío.</value>
</data>
<data name="ExtractPlainTextButton" xml:space="preserve">
<value>Extraer y visualizar</value>
</data>
<data name="ExtractMarkdownButton" xml:space="preserve">
<value>Extraer en Markdown</value>
</data>
<data name="DownloadPlainTextButton" xml:space="preserve">
<value>Descargar texto</value>
</data>
<data name="DownloadMarkdownButton" xml:space="preserve">
<value>Descargar Markdown</value>
</data>
<data name="PdfTextPreviewTitle" xml:space="preserve">
<value>Vista previa del contenido</value>
</data>
<data name="PdfTextFaqWhatTitle" xml:space="preserve">
<value>¿Qué hace este conversor?</value>
</data>
<data name="PdfTextFaqWhatContent" xml:space="preserve">
<value>Extrae el contenido textual del PDF y lo entrega como texto simple o Markdown, preservando cortes y secciones siempre que sea posible.</value>
</data>
<data name="PdfTextFaqHowTitle" xml:space="preserve">
<value>¿Cómo usar el conversor?</value>
</data>
<data name="PdfTextFaqHowContent" xml:space="preserve">
<value>Selecciona el archivo PDF, ingresa la contraseña si es necesario y elige la pestaña deseada. Visualiza el resultado en línea o descarga el archivo.</value>
</data>
<data name="PdfTextFaqWhyTitle" xml:space="preserve">
<value>¿Por qué convertir PDF a texto o Markdown?</value>
</data>
<data name="PdfTextFaqWhyContent" xml:space="preserve">
<value>El texto simple permite ediciones rápidas, mientras que Markdown facilita reutilizar el contenido en blogs o documentación.</value>
</data>
<data name="PdfTextFaqSecurityTitle" xml:space="preserve">
<value>¿Es seguro subir mis PDFs?</value>
</data>
<data name="PdfTextFaqSecurityContent" xml:space="preserve">
<value>Sí. Procesamos los archivos siguiendo estrictas políticas de privacidad y los eliminamos tras la conversión.</value>
</data>
<data name="PdfTextFaqLimitsTitle" xml:space="preserve">
<value>¿Existen límites de tamaño?</value>
</data>
<data name="PdfTextFaqLimitsContent" xml:space="preserve">
<value>Para la vista previa recomendamos PDFs de hasta 10MB. Archivos mayores pueden descargarse directamente en texto o Markdown.</value>
</data>
<!-- PDF Barcode Converter Resources -->
<data name="PdfBarcodeConverterPageTitle" xml:space="preserve">
<value>Extraer línea digitada de PDF</value>
</data>
<data name="PdfBarcodeConverterPageDescription" xml:space="preserve">
<value>Detecta la línea digitada de boletos o facturas directamente desde el PDF, incluso si está protegido.</value>
</data>
<data name="PdfBarcodeDetectTabTitle" xml:space="preserve">
<value>PDF → Ver línea digitada</value>
</data>
<data name="PdfBarcodeDownloadTabTitle" xml:space="preserve">
<value>PDF → Descargar línea digitada</value>
</data>
<data name="PdfBarcodeFileInputLabel" xml:space="preserve">
<value>Seleccionar PDF del boleto</value>
</data>
<data name="PdfBarcodePasswordHint" xml:space="preserve">
<value>Si el PDF está protegido con contraseña, ingrésala para permitir la lectura.</value>
</data>
<data name="DetectBarcodeButton" xml:space="preserve">
<value>Detectar línea digitada</value>
</data>
<data name="DownloadBarcodeButton" xml:space="preserve">
<value>Descargar línea digitada</value>
</data>
<data name="PdfBarcodePreviewTitle" xml:space="preserve">
<value>Línea digitada detectada</value>
</data>
<data name="PdfBarcodeFaqWhatTitle" xml:space="preserve">
<value>¿Qué es la línea digitada?</value>
</data>
<data name="PdfBarcodeFaqWhatContent" xml:space="preserve">
<value>Es la secuencia numérica del código de barras del boleto, utilizada para realizar pagos en bancos y aplicaciones.</value>
</data>
<data name="PdfBarcodeFaqHowTitle" xml:space="preserve">
<value>¿Cómo funciona el conversor?</value>
</data>
<data name="PdfBarcodeFaqHowContent" xml:space="preserve">
<value>Extrae el texto del PDF, identifica patrones de boletos (47 o 48 dígitos) y muestra la línea lista para copiar o descargar.</value>
</data>
<data name="PdfBarcodeFaqWhyTitle" xml:space="preserve">
<value>¿Cuándo usar este recurso?</value>
</data>
<data name="PdfBarcodeFaqWhyContent" xml:space="preserve">
<value>Ideal para boletos sin código de barras legible o cuando necesitas copiar la línea digitada para pagar en línea.</value>
</data>
<data name="PdfBarcodeFaqSecurityTitle" xml:space="preserve">
<value>¿Mis datos están seguros?</value>
</data>
<data name="PdfBarcodeFaqSecurityContent" xml:space="preserve">
<value>Sí. El PDF se procesa solo para extraer la línea digitada y se descarta inmediatamente después.</value>
</data>
<data name="PdfBarcodeFaqLimitsTitle" xml:space="preserve">
<value>¿Existen limitaciones?</value>
</data>
<data name="PdfBarcodeFaqLimitsContent" xml:space="preserve">
<value>Para mejores resultados utiliza PDFs con texto seleccionable. Boletos en formato de imagen pueden requerir una solución OCR externa.</value>
</data>
<!-- Error Messages --> <!-- Error Messages -->
<data name="SelectFileError" xml:space="preserve"> <data name="SelectFileError" xml:space="preserve">
<value>Por favor, selecciona un archivo para convertir.</value> <value>Por favor, selecciona un archivo para convertir.</value>
</data> </data>
<data name="CopyButtonLabel" xml:space="preserve">
<value>Copiar</value>
</data>
<data name="CopyButtonSuccess" xml:space="preserve">
<value>¡Copiado!</value>
</data>
<data name="InvalidHeicFileError" xml:space="preserve"> <data name="InvalidHeicFileError" xml:space="preserve">
<value>Archivo HEIC inválido o formato no compatible. Revisa el archivo o intenta con otro. La biblioteca nativa 'libheif' podría faltar en el sistema.</value> <value>Archivo HEIC inválido o formato no compatible. Revisa el archivo o intenta con otro. La biblioteca nativa 'libheif' podría faltar en el sistema.</value>
</data> </data>
<data name="InvalidJpgFileError" xml:space="preserve"> <data name="InvalidJpgFileError" xml:space="preserve">
<value>Archivo JPG inválido o formato no compatible. Verifica que el archivo sea una imagen JPG válida.</value> <value>Archivo JPG inválido o formato no compatible. Verifica que el archivo sea una imagen JPG válida.</value>
</data> </data>
<data name="InvalidPdfFileError" xml:space="preserve">
<value>Archivo PDF inválido, dañado o con protecciones no compatibles.</value>
</data>
<data name="PdfPasswordRequired" xml:space="preserve">
<value>Este PDF está protegido. Ingresa la contraseña para continuar.</value>
</data>
<data name="PdfInvalidPassword" xml:space="preserve">
<value>La contraseña ingresada no pudo desbloquear el PDF.</value>
</data>
<data name="PdfNoBarcodeFound" xml:space="preserve">
<value>No encontramos una línea digitada en este PDF.</value>
</data>
<data name="PdfPreviewTooLarge" xml:space="preserve">
<value>El contenido es demasiado grande para la vista previa. Descarga el resultado directamente.</value>
</data>
<!-- JPG to WebP Converter Resources --> <!-- JPG to WebP Converter Resources -->
<data name="JpgWebpConverterPageTitle" xml:space="preserve"> <data name="JpgWebpConverterPageTitle" xml:space="preserve">
<value>Convertidor JPG ↔ WebP Online Gratis</value> <value>Convertidor JPG ↔ WebP Online Gratis</value>

View File

@ -51,21 +51,12 @@
<data name="ImageMenuTitle" xml:space="preserve"> <data name="ImageMenuTitle" xml:space="preserve">
<value>Imagen</value> <value>Imagen</value>
</data> </data>
<data name="DocumentMenuTitle" xml:space="preserve">
<value>Documentos</value>
</data>
<data name="CaseConverterTitle" xml:space="preserve"> <data name="CaseConverterTitle" xml:space="preserve">
<value>Convertidor de Mayúsculas/Minúsculas</value> <value>Convertidor de Mayúsculas/Minúsculas</value>
</data> </data>
<data name="JpgToWebpTitle" xml:space="preserve"> <data name="JpgToWebpTitle" xml:space="preserve">
<value>JPG a WebP</value> <value>JPG a WebP</value>
</data> </data>
<data name="PdfToTextTitle" xml:space="preserve">
<value>PDF a Texto/Markdown</value>
</data>
<data name="PdfBarcodeTitle" xml:space="preserve">
<value>Detectar línea digitada</value>
</data>
<!-- Footer --> <!-- Footer -->
<data name="FooterText" xml:space="preserve"> <data name="FooterText" xml:space="preserve">
@ -122,21 +113,12 @@
<data name="UrlImageConverters" xml:space="preserve"> <data name="UrlImageConverters" xml:space="preserve">
<value>convertidores-de-imagen</value> <value>convertidores-de-imagen</value>
</data> </data>
<data name="UrlDocumentConverters" xml:space="preserve">
<value>convertidores-de-documento</value>
</data>
<data name="UrlCaseConverter" xml:space="preserve"> <data name="UrlCaseConverter" xml:space="preserve">
<value>conversor-de-mayusculas-minusculas</value> <value>conversor-de-mayusculas-minusculas</value>
</data> </data>
<data name="UrlJpgToWebp" xml:space="preserve"> <data name="UrlJpgToWebp" xml:space="preserve">
<value>jpg-a-webp</value> <value>jpg-a-webp</value>
</data> </data>
<data name="UrlPdfToText" xml:space="preserve">
<value>pdf-a-texto</value>
</data>
<data name="UrlPdfBarcode" xml:space="preserve">
<value>linea-digitada-desde-pdf</value>
</data>
<!-- New individual converter resources - Mexican Spanish --> <!-- New individual converter resources - Mexican Spanish -->
<data name="AllConvertersTitle" xml:space="preserve"> <data name="AllConvertersTitle" xml:space="preserve">
@ -160,18 +142,6 @@
<data name="HeicToJpgIndividualDescription" xml:space="preserve"> <data name="HeicToJpgIndividualDescription" xml:space="preserve">
<value>Convierte fotos HEIC del iPhone al formato JPG universalmente compatible.</value> <value>Convierte fotos HEIC del iPhone al formato JPG universalmente compatible.</value>
</data> </data>
<data name="PdfToTextIndividualTitle" xml:space="preserve">
<value>Extraer texto de PDF</value>
</data>
<data name="PdfToTextIndividualDescription" xml:space="preserve">
<value>Transforma tus PDFs en texto simple o Markdown, incluso cuando requieren contraseña.</value>
</data>
<data name="PdfBarcodeIndividualTitle" xml:space="preserve">
<value>Detectar línea digitada</value>
</data>
<data name="PdfBarcodeIndividualDescription" xml:space="preserve">
<value>Obtén la línea digitada de boletos directamente desde el PDF, con soporte para archivos protegidos.</value>
</data>
<!-- HEIC converter menu item --> <!-- HEIC converter menu item -->
<data name="HeicToJpgTitle" xml:space="preserve"> <data name="HeicToJpgTitle" xml:space="preserve">
@ -234,167 +204,16 @@
<value>No hay límites en la cantidad de conversiones. Para garantizar un buen rendimiento, recomendamos archivos de hasta 10MB. El procesamiento es más rápido en computadoras modernas.</value> <value>No hay límites en la cantidad de conversiones. Para garantizar un buen rendimiento, recomendamos archivos de hasta 10MB. El procesamiento es más rápido en computadoras modernas.</value>
</data> </data>
<!-- PDF to Text Converter Resources -->
<data name="PdfTextConverterPageTitle" xml:space="preserve">
<value>Extraer texto de PDF</value>
</data>
<data name="PdfTextConverterPageDescription" xml:space="preserve">
<value>Convierte tus PDFs en texto simple o Markdown con soporte para archivos protegidos con contraseña.</value>
</data>
<data name="PdfPlainTextTabTitle" xml:space="preserve">
<value>PDF → Texto simple</value>
</data>
<data name="PdfMarkdownTabTitle" xml:space="preserve">
<value>PDF → Markdown</value>
</data>
<data name="PdfFileInputLabel" xml:space="preserve">
<value>Seleccionar archivo PDF</value>
</data>
<data name="PdfPasswordLabel" xml:space="preserve">
<value>Contraseña del PDF</value>
</data>
<data name="PdfPasswordPlaceholder" xml:space="preserve">
<value>Ingresa la contraseña si el PDF está protegido</value>
</data>
<data name="PdfPasswordHint" xml:space="preserve">
<value>Si el PDF requiere contraseña de apertura, escríbela aquí. De lo contrario, deja el campo vacío.</value>
</data>
<data name="ExtractPlainTextButton" xml:space="preserve">
<value>Extraer y visualizar</value>
</data>
<data name="ExtractMarkdownButton" xml:space="preserve">
<value>Extraer en Markdown</value>
</data>
<data name="DownloadPlainTextButton" xml:space="preserve">
<value>Descargar texto</value>
</data>
<data name="DownloadMarkdownButton" xml:space="preserve">
<value>Descargar Markdown</value>
</data>
<data name="PdfTextPreviewTitle" xml:space="preserve">
<value>Vista previa del contenido</value>
</data>
<data name="PdfTextFaqWhatTitle" xml:space="preserve">
<value>¿Qué hace este conversor?</value>
</data>
<data name="PdfTextFaqWhatContent" xml:space="preserve">
<value>Extrae el contenido textual del PDF y lo entrega como texto simple o Markdown, preservando cortes y secciones siempre que sea posible.</value>
</data>
<data name="PdfTextFaqHowTitle" xml:space="preserve">
<value>¿Cómo usar el conversor?</value>
</data>
<data name="PdfTextFaqHowContent" xml:space="preserve">
<value>Selecciona el archivo PDF, ingresa la contraseña si es necesario y elige la pestaña deseada. Visualiza el resultado en línea o descarga el archivo.</value>
</data>
<data name="PdfTextFaqWhyTitle" xml:space="preserve">
<value>¿Por qué convertir PDF a texto o Markdown?</value>
</data>
<data name="PdfTextFaqWhyContent" xml:space="preserve">
<value>El texto simple permite ediciones rápidas, mientras que Markdown facilita reutilizar el contenido en blogs o documentación.</value>
</data>
<data name="PdfTextFaqSecurityTitle" xml:space="preserve">
<value>¿Es seguro subir mis PDFs?</value>
</data>
<data name="PdfTextFaqSecurityContent" xml:space="preserve">
<value>Sí. Procesamos los archivos siguiendo estrictas políticas de privacidad y los eliminamos tras la conversión.</value>
</data>
<data name="PdfTextFaqLimitsTitle" xml:space="preserve">
<value>¿Existen límites de tamaño?</value>
</data>
<data name="PdfTextFaqLimitsContent" xml:space="preserve">
<value>Para la vista previa recomendamos PDFs de hasta 10MB. Archivos mayores pueden descargarse directamente en texto o Markdown.</value>
</data>
<!-- PDF Barcode Converter Resources -->
<data name="PdfBarcodeConverterPageTitle" xml:space="preserve">
<value>Extraer línea digitada de PDF</value>
</data>
<data name="PdfBarcodeConverterPageDescription" xml:space="preserve">
<value>Detecta la línea digitada de boletos o facturas directamente desde el PDF, incluso si está protegido.</value>
</data>
<data name="PdfBarcodeDetectTabTitle" xml:space="preserve">
<value>PDF → Ver línea digitada</value>
</data>
<data name="PdfBarcodeDownloadTabTitle" xml:space="preserve">
<value>PDF → Descargar línea digitada</value>
</data>
<data name="PdfBarcodeFileInputLabel" xml:space="preserve">
<value>Seleccionar PDF del boleto</value>
</data>
<data name="PdfBarcodePasswordHint" xml:space="preserve">
<value>Si el PDF está protegido con contraseña, ingrésala para permitir la lectura.</value>
</data>
<data name="DetectBarcodeButton" xml:space="preserve">
<value>Detectar línea digitada</value>
</data>
<data name="DownloadBarcodeButton" xml:space="preserve">
<value>Descargar línea digitada</value>
</data>
<data name="PdfBarcodePreviewTitle" xml:space="preserve">
<value>Línea digitada detectada</value>
</data>
<data name="PdfBarcodeFaqWhatTitle" xml:space="preserve">
<value>¿Qué es la línea digitada?</value>
</data>
<data name="PdfBarcodeFaqWhatContent" xml:space="preserve">
<value>Es la secuencia numérica del código de barras del boleto, utilizada para realizar pagos en bancos y aplicaciones.</value>
</data>
<data name="PdfBarcodeFaqHowTitle" xml:space="preserve">
<value>¿Cómo funciona el conversor?</value>
</data>
<data name="PdfBarcodeFaqHowContent" xml:space="preserve">
<value>Extrae el texto del PDF, identifica patrones de boletos (47 o 48 dígitos) y muestra la línea lista para copiar o descargar.</value>
</data>
<data name="PdfBarcodeFaqWhyTitle" xml:space="preserve">
<value>¿Cuándo usar este recurso?</value>
</data>
<data name="PdfBarcodeFaqWhyContent" xml:space="preserve">
<value>Ideal para boletos sin código de barras legible o cuando necesitas copiar la línea digitada para pagar en línea.</value>
</data>
<data name="PdfBarcodeFaqSecurityTitle" xml:space="preserve">
<value>¿Mis datos están seguros?</value>
</data>
<data name="PdfBarcodeFaqSecurityContent" xml:space="preserve">
<value>Sí. El PDF se procesa solo para extraer la línea digitada y se descarta inmediatamente después.</value>
</data>
<data name="PdfBarcodeFaqLimitsTitle" xml:space="preserve">
<value>¿Existen limitaciones?</value>
</data>
<data name="PdfBarcodeFaqLimitsContent" xml:space="preserve">
<value>Para mejores resultados utiliza PDFs con texto seleccionable. Boletos en formato de imagen pueden requerir una solución OCR externa.</value>
</data>
<!-- Error Messages --> <!-- Error Messages -->
<data name="SelectFileError" xml:space="preserve"> <data name="SelectFileError" xml:space="preserve">
<value>Por favor, selecciona un archivo para convertir.</value> <value>Por favor, selecciona un archivo para convertir.</value>
</data> </data>
<data name="CopyButtonLabel" xml:space="preserve">
<value>Copiar</value>
</data>
<data name="CopyButtonSuccess" xml:space="preserve">
<value>¡Copiado!</value>
</data>
<data name="InvalidHeicFileError" xml:space="preserve"> <data name="InvalidHeicFileError" xml:space="preserve">
<value>Archivo HEIC inválido o formato no compatible. Revisa el archivo o intenta con otro. La biblioteca nativa 'libheif' podría faltar en el sistema.</value> <value>Archivo HEIC inválido o formato no compatible. Revisa el archivo o intenta con otro. La biblioteca nativa 'libheif' podría faltar en el sistema.</value>
</data> </data>
<data name="InvalidJpgFileError" xml:space="preserve"> <data name="InvalidJpgFileError" xml:space="preserve">
<value>Archivo JPG inválido o formato no compatible. Verifica que el archivo sea una imagen JPG válida.</value> <value>Archivo JPG inválido o formato no compatible. Verifica que el archivo sea una imagen JPG válida.</value>
</data> </data>
<data name="InvalidPdfFileError" xml:space="preserve">
<value>Archivo PDF inválido, dañado o con protecciones no compatibles.</value>
</data>
<data name="PdfPasswordRequired" xml:space="preserve">
<value>Este PDF está protegido. Ingresa la contraseña para continuar.</value>
</data>
<data name="PdfInvalidPassword" xml:space="preserve">
<value>La contraseña ingresada no pudo desbloquear el PDF.</value>
</data>
<data name="PdfNoBarcodeFound" xml:space="preserve">
<value>No encontramos una línea digitada en este PDF.</value>
</data>
<data name="PdfPreviewTooLarge" xml:space="preserve">
<value>El contenido es demasiado grande para la vista previa. Descarga el resultado directamente.</value>
</data>
<!-- JPG to WebP Converter Resources --> <!-- JPG to WebP Converter Resources -->
<data name="JpgWebpConverterPageTitle" xml:space="preserve"> <data name="JpgWebpConverterPageTitle" xml:space="preserve">
<value>Convertidor JPG ↔ WebP Online Gratis</value> <value>Convertidor JPG ↔ WebP Online Gratis</value>

View File

@ -183,21 +183,12 @@
<data name="ImageMenuTitle" xml:space="preserve"> <data name="ImageMenuTitle" xml:space="preserve">
<value>Imagem</value> <value>Imagem</value>
</data> </data>
<data name="DocumentMenuTitle" xml:space="preserve">
<value>Documentos</value>
</data>
<data name="CaseConverterTitle" xml:space="preserve"> <data name="CaseConverterTitle" xml:space="preserve">
<value>Conversor de Maiúsculas/Minúsculas</value> <value>Conversor de Maiúsculas/Minúsculas</value>
</data> </data>
<data name="JpgToWebpTitle" xml:space="preserve"> <data name="JpgToWebpTitle" xml:space="preserve">
<value>JPG para WebP</value> <value>JPG para WebP</value>
</data> </data>
<data name="PdfToTextTitle" xml:space="preserve">
<value>PDF para Texto/Markdown</value>
</data>
<data name="PdfBarcodeTitle" xml:space="preserve">
<value>Extrair Linha Digitável</value>
</data>
<!-- Footer Resources --> <!-- Footer Resources -->
<data name="FooterText" xml:space="preserve"> <data name="FooterText" xml:space="preserve">
@ -220,24 +211,12 @@
<data name="UrlImageConverters" xml:space="preserve"> <data name="UrlImageConverters" xml:space="preserve">
<value>conversores-de-imagem</value> <value>conversores-de-imagem</value>
</data> </data>
<data name="UrlDocumentConverters" xml:space="preserve">
<value>conversores-de-documento</value>
</data>
<data name="UrlCaseConverter" xml:space="preserve"> <data name="UrlCaseConverter" xml:space="preserve">
<value>conversor-de-maiusculas-minusculas</value> <value>conversor-de-maiusculas-minusculas</value>
</data> </data>
<data name="UrlJpgToWebp" xml:space="preserve"> <data name="UrlJpgToWebp" xml:space="preserve">
<value>jpg-para-webp</value> <value>jpg-para-webp</value>
</data> </data>
<data name="UrlHeicToJpg" xml:space="preserve">
<value>heic-para-jpg</value>
</data>
<data name="UrlPdfToText" xml:space="preserve">
<value>pdf-para-texto</value>
</data>
<data name="UrlPdfBarcode" xml:space="preserve">
<value>linha-digitavel-do-pdf</value>
</data>
<!-- FAQ Resources for Contact Page --> <!-- FAQ Resources for Contact Page -->
<data name="Faq1Question" xml:space="preserve"> <data name="Faq1Question" xml:space="preserve">
@ -275,18 +254,6 @@
<data name="HeicToJpgIndividualDescription" xml:space="preserve"> <data name="HeicToJpgIndividualDescription" xml:space="preserve">
<value>Converta fotos HEIC do iPhone para o formato JPG universalmente compatível.</value> <value>Converta fotos HEIC do iPhone para o formato JPG universalmente compatível.</value>
</data> </data>
<data name="PdfToTextIndividualTitle" xml:space="preserve">
<value>Extrair Texto de PDF</value>
</data>
<data name="PdfToTextIndividualDescription" xml:space="preserve">
<value>Transforme PDFs protegidos ou não em texto simples ou Markdown em poucos segundos.</value>
</data>
<data name="PdfBarcodeIndividualTitle" xml:space="preserve">
<value>Detectar Linha Digitável</value>
</data>
<data name="PdfBarcodeIndividualDescription" xml:space="preserve">
<value>Identifique a linha digitável de boletos diretamente do PDF, mesmo quando há proteção por senha.</value>
</data>
<!-- HEIC converter menu item --> <!-- HEIC converter menu item -->
<data name="HeicToJpgTitle" xml:space="preserve"> <data name="HeicToJpgTitle" xml:space="preserve">
@ -349,136 +316,6 @@
<value>Não há limites para a quantidade de conversões. Para garantir um bom desempenho, recomendamos arquivos de até 10MB. O processamento é mais rápido em computadores modernos.</value> <value>Não há limites para a quantidade de conversões. Para garantir um bom desempenho, recomendamos arquivos de até 10MB. O processamento é mais rápido em computadores modernos.</value>
</data> </data>
<!-- PDF to Text Converter Resources -->
<data name="PdfTextConverterPageTitle" xml:space="preserve">
<value>Extrair Texto de PDF</value>
</data>
<data name="PdfTextConverterPageDescription" xml:space="preserve">
<value>Converta seus PDFs em texto simples ou Markdown, com suporte a arquivos protegidos por senha.</value>
</data>
<data name="PdfPlainTextTabTitle" xml:space="preserve">
<value>PDF → Texto Simples</value>
</data>
<data name="PdfMarkdownTabTitle" xml:space="preserve">
<value>PDF → Markdown</value>
</data>
<data name="PdfFileInputLabel" xml:space="preserve">
<value>Selecionar arquivo PDF</value>
</data>
<data name="PdfPasswordLabel" xml:space="preserve">
<value>Senha do PDF</value>
</data>
<data name="PdfPasswordPlaceholder" xml:space="preserve">
<value>Informe a senha, se o PDF estiver protegido</value>
</data>
<data name="PdfPasswordHint" xml:space="preserve">
<value>Se o PDF exigir senha de abertura, digite-a abaixo. Caso contrário, deixe em branco.</value>
</data>
<data name="ExtractPlainTextButton" xml:space="preserve">
<value>Extrair e Visualizar</value>
</data>
<data name="ExtractMarkdownButton" xml:space="preserve">
<value>Extrair em Markdown</value>
</data>
<data name="DownloadPlainTextButton" xml:space="preserve">
<value>Baixar Texto</value>
</data>
<data name="DownloadMarkdownButton" xml:space="preserve">
<value>Baixar Markdown</value>
</data>
<data name="PdfTextPreviewTitle" xml:space="preserve">
<value>Pré-visualização do Conteúdo</value>
</data>
<data name="PdfTextFaqWhatTitle" xml:space="preserve">
<value>O que faz este conversor PDF para Texto?</value>
</data>
<data name="PdfTextFaqWhatContent" xml:space="preserve">
<value>Ele extrai o conteúdo textual do seu PDF e o disponibiliza como texto simples ou Markdown, preservando quebras de linha e seções sempre que possível.</value>
</data>
<data name="PdfTextFaqHowTitle" xml:space="preserve">
<value>Como usar o conversor?</value>
</data>
<data name="PdfTextFaqHowContent" xml:space="preserve">
<value>Selecione o arquivo PDF, informe a senha se necessário e escolha a aba desejada. Visualize o resultado online ou faça o download direto.</value>
</data>
<data name="PdfTextFaqWhyTitle" xml:space="preserve">
<value>Por que converter PDF para texto ou Markdown?</value>
</data>
<data name="PdfTextFaqWhyContent" xml:space="preserve">
<value>Texto simples facilita edições rápidas, enquanto Markdown permite reaproveitar o conteúdo em blogs, documentações e geradores de site estático.</value>
</data>
<data name="PdfTextFaqSecurityTitle" xml:space="preserve">
<value>É seguro enviar meus PDFs?</value>
</data>
<data name="PdfTextFaqSecurityContent" xml:space="preserve">
<value>Sim. O processamento ocorre seguindo políticas rígidas de privacidade e os arquivos são descartados após a conversão.</value>
</data>
<data name="PdfTextFaqLimitsTitle" xml:space="preserve">
<value>Existem limites de tamanho?</value>
</data>
<data name="PdfTextFaqLimitsContent" xml:space="preserve">
<value>Para pré-visualização recomendamos PDFs de até 10MB. Conteúdos maiores podem ser baixados diretamente em texto ou Markdown.</value>
</data>
<!-- PDF Barcode Converter Resources -->
<data name="PdfBarcodeConverterPageTitle" xml:space="preserve">
<value>Extrair Linha Digitável de PDF</value>
</data>
<data name="PdfBarcodeConverterPageDescription" xml:space="preserve">
<value>Detecte a linha digitável de boletos ou faturas diretamente a partir do PDF, com suporte a arquivos protegidos.</value>
</data>
<data name="PdfBarcodeDetectTabTitle" xml:space="preserve">
<value>PDF → Visualizar Linha Digitável</value>
</data>
<data name="PdfBarcodeDownloadTabTitle" xml:space="preserve">
<value>PDF → Baixar Linha Digitável</value>
</data>
<data name="PdfBarcodeFileInputLabel" xml:space="preserve">
<value>Selecionar PDF do boleto</value>
</data>
<data name="PdfBarcodePasswordHint" xml:space="preserve">
<value>Caso o PDF esteja protegido por senha, informe-a abaixo para permitir a leitura.</value>
</data>
<data name="DetectBarcodeButton" xml:space="preserve">
<value>Detectar Linha Digitável</value>
</data>
<data name="DownloadBarcodeButton" xml:space="preserve">
<value>Baixar Linha Digitável</value>
</data>
<data name="PdfBarcodePreviewTitle" xml:space="preserve">
<value>Linha Digitável Detectada</value>
</data>
<data name="PdfBarcodeFaqWhatTitle" xml:space="preserve">
<value>O que é a linha digitável?</value>
</data>
<data name="PdfBarcodeFaqWhatContent" xml:space="preserve">
<value>É a sequência numérica que representa o código de barras de boletos e faturas, usada para pagamentos em bancos e aplicativos.</value>
</data>
<data name="PdfBarcodeFaqHowTitle" xml:space="preserve">
<value>Como o conversor funciona?</value>
</data>
<data name="PdfBarcodeFaqHowContent" xml:space="preserve">
<value>O sistema extrai o texto do PDF, identifica padrões numéricos de boletos (47 ou 48 dígitos) e exibe a linha digitável pronta para copiar ou baixar.</value>
</data>
<data name="PdfBarcodeFaqWhyTitle" xml:space="preserve">
<value>Quando usar este recurso?</value>
</data>
<data name="PdfBarcodeFaqWhyContent" xml:space="preserve">
<value>Ideal para boletos sem código de barras legível ou quando você precisa copiar a linha digitável para pagamento online rapidamente.</value>
</data>
<data name="PdfBarcodeFaqSecurityTitle" xml:space="preserve">
<value>Meus dados ficam seguros?</value>
</data>
<data name="PdfBarcodeFaqSecurityContent" xml:space="preserve">
<value>Sim. O PDF é processado apenas para extrair a linha digitável e o conteúdo é descartado logo após a operação.</value>
</data>
<data name="PdfBarcodeFaqLimitsTitle" xml:space="preserve">
<value>Existe algum limite?</value>
</data>
<data name="PdfBarcodeFaqLimitsContent" xml:space="preserve">
<value>Para melhores resultados, utilize PDFs com texto selecionável. Boletos apenas com imagem podem exigir uma solução OCR externa.</value>
</data>
<!-- JPG to WebP Converter Resources --> <!-- JPG to WebP Converter Resources -->
<data name="JpgWebpConverterPageTitle" xml:space="preserve"> <data name="JpgWebpConverterPageTitle" xml:space="preserve">
<value>Conversor JPG ↔ WebP Online Grátis</value> <value>Conversor JPG ↔ WebP Online Grátis</value>
@ -533,12 +370,6 @@
<data name="UseConverterButton" xml:space="preserve"> <data name="UseConverterButton" xml:space="preserve">
<value>Acessar Conversor</value> <value>Acessar Conversor</value>
</data> </data>
<data name="CopyButtonLabel" xml:space="preserve">
<value>Copiar</value>
</data>
<data name="CopyButtonSuccess" xml:space="preserve">
<value>Copiado!</value>
</data>
<!-- Error Messages --> <!-- Error Messages -->
<data name="SelectFileError" xml:space="preserve"> <data name="SelectFileError" xml:space="preserve">
@ -553,21 +384,6 @@
<data name="JpgToHeicNotAvailableError" xml:space="preserve"> <data name="JpgToHeicNotAvailableError" xml:space="preserve">
<value>A conversão de JPG para HEIC ainda não está disponível. Esta funcionalidade será implementada em breve.</value> <value>A conversão de JPG para HEIC ainda não está disponível. Esta funcionalidade será implementada em breve.</value>
</data> </data>
<data name="InvalidPdfFileError" xml:space="preserve">
<value>Arquivo PDF inválido, corrompido ou protegido por recursos não suportados.</value>
</data>
<data name="PdfPasswordRequired" xml:space="preserve">
<value>Este PDF está protegido. Informe a senha para continuar.</value>
</data>
<data name="PdfInvalidPassword" xml:space="preserve">
<value>A senha informada não conseguiu desbloquear o PDF.</value>
</data>
<data name="PdfNoBarcodeFound" xml:space="preserve">
<value>Não encontramos uma linha digitável neste PDF.</value>
</data>
<data name="PdfPreviewTooLarge" xml:space="preserve">
<value>O conteúdo é muito grande para pré-visualização. Faça o download direto.</value>
</data>
<data name="ProcessingError" xml:space="preserve"> <data name="ProcessingError" xml:space="preserve">
<value>Ocorreu um erro ao processar sua solicitação. Tente novamente mais tarde.</value> <value>Ocorreu um erro ao processar sua solicitação. Tente novamente mais tarde.</value>
</data> </data>

View File

@ -1,21 +1,11 @@
@using Microsoft.AspNetCore.Localization @using Microsoft.AspNetCore.Localization
@using Microsoft.Extensions.Options @using Microsoft.Extensions.Options
@using Microsoft.Extensions.Configuration
@inject IOptions<RequestLocalizationOptions> LocOptions @inject IOptions<RequestLocalizationOptions> LocOptions
@inject IConfiguration Configuration
@{ @{
var requestCulture = Context.Features.Get<IRequestCultureFeature>(); var requestCulture = Context.Features.Get<IRequestCultureFeature>();
var supportedCultures = LocOptions.Value.SupportedCultures; var supportedCultures = LocOptions.Value.SupportedCultures;
// Configuração de Ads
var adSection = Configuration.GetSection("Ads");
var adEnabled = adSection.GetValue<bool>("Enabled");
var adProvider = adSection.GetValue<string>("Provider"); // "House" ou "Google"
var googlePublisherId = adSection.GetValue<string>("GooglePublisherId");
var slotTop = adSection.GetValue<string>("Slots:Top");
var slotSide = adSection.GetValue<string>("Slots:Side");
} }
<!DOCTYPE html> <!DOCTYPE html>
@ -23,19 +13,11 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="@(ViewBag.MetaDescription ?? ViewBag.PageDescription ?? ViewBag.Subtitle ?? ViewBag.PageTitle ?? "Convert-It Online")" />
<title>@ViewData["Title"] - Convert-It Online</title> <title>@ViewData["Title"] - Convert-It Online</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" /> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<link rel="stylesheet" href="~/css/site.css" /> <link rel="stylesheet" href="~/css/site.css" />
<link rel="manifest" href="~/manifest.json" />
@if (adEnabled && adProvider == "Google" && !string.IsNullOrEmpty(googlePublisherId))
{
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=@googlePublisherId"
crossorigin="anonymous"></script>
}
</head> </head>
<body> <body>
<header> <header>
@ -60,19 +42,6 @@
</a></li> </a></li>
</ul> </ul>
</li> </li>
<li class="nav-item dropdown mx-2">
<a class="nav-link dropdown-toggle" href="#" id="documentToolsDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-file-earmark-text me-1"></i>@ViewBag.DocumentMenuTitle
</a>
<ul class="dropdown-menu" aria-labelledby="documentToolsDropdown">
<li><a class="dropdown-item" href="@Html.LocalizedUrl("DocumentConverters", "PdfToText")">
<i class="bi bi-file-text me-2"></i>@ViewBag.PdfToTextTitle
</a></li>
<li><a class="dropdown-item" href="@Html.LocalizedUrl("DocumentConverters", "PdfBarcodeLine")">
<i class="bi bi-upc-scan me-2"></i>@ViewBag.PdfBarcodeTitle
</a></li>
</ul>
</li>
<li class="nav-item dropdown mx-2"> <li class="nav-item dropdown mx-2">
<a class="nav-link dropdown-toggle" href="#" id="imageToolsDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false"> <a class="nav-link dropdown-toggle" href="#" id="imageToolsDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-image-alt me-1"></i>@ViewBag.ImageMenuTitle <i class="bi bi-image-alt me-1"></i>@ViewBag.ImageMenuTitle
@ -86,19 +55,6 @@
</a></li> </a></li>
</ul> </ul>
</li> </li>
<li class="nav-item dropdown mx-2">
<a class="nav-link dropdown-toggle" href="#" id="audioToolsDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-mic me-1"></i>Áudio
</a>
<ul class="dropdown-menu" aria-labelledby="audioToolsDropdown">
<li><a class="dropdown-item" href="@Html.LocalizedUrl("AudioTools", "SpeechToText")">
<i class="bi bi-chat-left-text me-2"></i>Áudio para Texto
</a></li>
<li><a class="dropdown-item" href="@Html.LocalizedUrl("AudioTools", "TextToSpeech")">
<i class="bi bi-megaphone me-2"></i>Texto para Áudio
</a></li>
</ul>
</li>
</ul> </ul>
<div class="dropdown"> <div class="dropdown">
<button class="btn btn-secondary dropdown-toggle" type="button" id="languageDropdown" data-bs-toggle="dropdown" aria-expanded="false"> <button class="btn btn-secondary dropdown-toggle" type="button" id="languageDropdown" data-bs-toggle="dropdown" aria-expanded="false">
@ -128,98 +84,17 @@
<div class="container mt-4"> <div class="container mt-4">
<div class="row"> <div class="row">
<div class="col-lg-9"> <div class="col-lg-9">
<!-- AD TOP --> <div class="ad-placeholder ad-top d-none d-lg-flex">
@if (adEnabled) <span>Ad Placeholder (Top)</span>
{
@if (adProvider == "House")
{
<a href="https://qrrapido.site" target="_blank" class="house-ad-top text-decoration-none d-none d-lg-block">
<div class="ad-content">
<div class="qr-animation">
<div class="qr-box"></div>
<div class="scan-line"></div>
</div> </div>
<div class="ad-text">
<span class="badge bg-warning text-dark mb-1">NOVO</span>
<h5>Gerador de QR Code Ultrarrápido</h5>
<p>Crie QR Codes para WiFi, Links e Pix em 1 segundo. Grátis!</p>
</div>
<div class="cta-button">
Criar Agora <i class="bi bi-arrow-right"></i>
</div>
</div>
</a>
}
else if (adProvider == "Google" && !string.IsNullOrEmpty(slotTop))
{
<div class="d-none d-lg-block mb-4" style="min-height: 90px; text-align:center;">
<!-- Topo -->
<ins class="adsbygoogle"
style="display:block"
data-ad-client="@googlePublisherId"
data-ad-slot="@slotTop"
data-ad-format="auto"
data-full-width-responsive="true"></ins>
<script>(adsbygoogle = window.adsbygoogle || []).push({});</script>
</div>
}
else
{
<!-- Placeholder visível apenas se não houver ad configurado e estiver em debug/dev se desejado -->
<!-- <div class="ad-placeholder ad-top d-none d-lg-flex"><span>Ad Space</span></div> -->
}
}
<main role="main" class="pb-3"> <main role="main" class="pb-3">
@RenderBody() @RenderBody()
</main> </main>
</div> </div>
<div class="col-lg-3 d-none d-lg-block"> <div class="col-lg-3 d-none d-lg-block">
<!-- AD SIDE --> <div class="ad-placeholder ad-side">
@if (adEnabled) <span>Ad Placeholder (Side)</span>
{
@if (adProvider == "House")
{
<a href="https://jobmaker.com.br" target="_blank" class="house-ad-side text-decoration-none">
<div class="terminal-header">
<span class="dot red"></span><span class="dot yellow"></span><span class="dot green"></span>
</div> </div>
<div class="ad-body">
<div class="code-typing">
<span class="comment">// Precisa integrar sistemas?</span><br>
<span class="keyword">await</span> JobMaker.<span class="method">Automate</span>(legacy_sys);<br>
<span class="success">>> Processo Otimizado!</span>
</div>
<div class="mt-4">
<h5>Especialistas em .NET e Integrações</h5>
<ul class="list-unstyled text-small text-muted mt-2">
<li><i class="bi bi-check2"></i> APIs & Microserviços</li>
<li><i class="bi bi-check2"></i> Robôs de Dados</li>
<li><i class="bi bi-check2"></i> Modernização de Legado</li>
</ul>
<button class="btn btn-outline-primary btn-sm w-100 mt-3">Fale Conosco</button>
</div>
</div>
</a>
}
else if (adProvider == "Google" && !string.IsNullOrEmpty(slotSide))
{
<div class="mt-4 sticky-top" style="top: 2rem; min-height: 600px;">
<!-- Lateral -->
<ins class="adsbygoogle"
style="display:block"
data-ad-client="@googlePublisherId"
data-ad-slot="@slotSide"
data-ad-format="auto"
data-full-width-responsive="true"></ins>
<script>(adsbygoogle = window.adsbygoogle || []).push({});</script>
</div>
}
else
{
<!-- <div class="ad-placeholder ad-side"><span>Ad Space</span></div> -->
}
}
</div> </div>
</div> </div>
</div> </div>
@ -240,11 +115,6 @@
</footer> </footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<script>
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js');
}
</script>
@await RenderSectionAsync("Scripts", required: false) @await RenderSectionAsync("Scripts", required: false)
</body> </body>
</html> </html>

View File

@ -9,14 +9,5 @@
"AllowedHosts": "*", "AllowedHosts": "*",
"Serilog": { "Serilog": {
"OpenSearchUrl": "" "OpenSearchUrl": ""
},
"Ads": {
"Enabled": true,
"Provider": "House",
"GooglePublisherId": "pub-3475956393038764",
"Slots": {
"Top": "0000000000",
"Side": "0000000000"
}
} }
} }

View File

@ -431,190 +431,3 @@ h1, h2, h3, h4, h5, h6, p, div, span, a {
::-webkit-scrollbar-thumb:hover { ::-webkit-scrollbar-thumb:hover {
background: var(--text-muted); background: var(--text-muted);
} }
/* ========== HOUSE ADS (BANNERS PRÓPRIOS) ========== */
/* Banner Topo - QR Rápido */
.house-ad-top {
display: block;
background: linear-gradient(135deg, #6610f2 0%, #d63384 100%);
border-radius: 1rem;
padding: 1rem 2rem;
color: white !important;
position: relative;
overflow: hidden;
transition: transform 0.3s ease, box-shadow 0.3s ease;
margin-bottom: 2rem;
border: none;
text-decoration: none !important;
}
.house-ad-top:hover {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(102, 16, 242, 0.3);
color: white !important;
}
.house-ad-top .ad-content {
display: flex;
align-items: center;
justify-content: space-between;
z-index: 2;
position: relative;
}
.house-ad-top .qr-animation {
width: 60px;
height: 60px;
background: rgba(255, 255, 255, 0.2);
border-radius: 8px;
position: relative;
display: flex;
align-items: center;
justify-content: center;
margin-right: 1.5rem;
}
.house-ad-top .qr-box {
width: 40px;
height: 40px;
border: 3px solid white;
border-radius: 4px;
position: relative;
}
.house-ad-top .qr-box::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 16px;
height: 16px;
background: white;
}
.house-ad-top .scan-line {
position: absolute;
width: 100%;
height: 2px;
background: #00ff00;
top: 10%;
left: 0;
animation: scanMove 1.5s infinite alternate ease-in-out;
box-shadow: 0 0 4px #00ff00;
}
@keyframes scanMove {
0% { top: 10%; }
100% { top: 90%; }
}
.house-ad-top .ad-text h5 {
margin: 0;
font-weight: 700;
font-size: 1.25rem;
color: white !important;
}
.house-ad-top .ad-text p {
margin: 0;
font-size: 0.9rem;
opacity: 0.9;
color: white !important;
}
.house-ad-top .cta-button {
background: white;
color: #6610f2;
padding: 0.5rem 1rem;
border-radius: 2rem;
font-weight: 600;
font-size: 0.9rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
/* Banner Lateral - JobMaker */
.house-ad-side {
display: block;
background-color: #0f172a; /* Slate 900 */
border-radius: 1rem;
border: 1px solid #1e293b;
color: #e2e8f0 !important;
overflow: hidden;
position: sticky;
top: 2rem;
text-decoration: none !important;
transition: border-color 0.3s ease;
min-height: auto; /* Remove min-height fixo se necessário */
}
.house-ad-side:hover {
border-color: #3b82f6;
color: #e2e8f0 !important;
}
.house-ad-side .terminal-header {
background: #1e293b;
padding: 0.75rem 1rem;
display: flex;
gap: 6px;
border-bottom: 1px solid #334155;
}
.house-ad-side .dot {
width: 10px;
height: 10px;
border-radius: 50%;
}
.house-ad-side .dot.red { background: #ef4444; }
.house-ad-side .dot.yellow { background: #f59e0b; }
.house-ad-side .dot.green { background: #22c55e; }
.house-ad-side .ad-body {
padding: 1.5rem;
font-family: 'Consolas', 'Monaco', monospace;
}
.house-ad-side .code-typing {
font-size: 0.85rem;
line-height: 1.6;
margin-bottom: 1.5rem;
}
.house-ad-side .comment { color: #64748b; }
.house-ad-side .keyword { color: #c084fc; }
.house-ad-side .method { color: #60a5fa; }
.house-ad-side .success { color: #4ade80; display: block; margin-top: 0.5rem; font-weight: bold;}
.house-ad-side h5 {
font-family: system-ui, -apple-system, sans-serif;
color: white !important;
font-weight: 700;
font-size: 1.1rem;
margin-bottom: 1rem;
}
.house-ad-side .text-muted {
color: #94a3b8 !important; /* Slate 400 */
}
.house-ad-side .btn-outline-primary {
border-color: #3b82f6;
color: #3b82f6;
}
.house-ad-side .btn-outline-primary:hover {
background-color: #3b82f6;
color: white;
}
/* Responsividade para o House Ad Top */
@media (max-width: 992px) {
.house-ad-top {
display: none;
}
}

View File

@ -1,29 +0,0 @@
{
"name": "Convert-It Online",
"short_name": "Convert-It",
"start_url": "/",
"display": "standalone",
"background_color": "#0d6efd",
"theme_color": "#0d6efd",
"description": "Conversores rápidos de imagem, documento e áudio.",
"icons": [
{
"src": "/favicon.ico",
"sizes": "64x64",
"type": "image/x-icon"
}
],
"share_target": {
"action": "/AudioTools/SpeechToText/HandleShare",
"method": "POST",
"enctype": "multipart/form-data",
"params": {
"files": [
{
"name": "audio",
"accept": ["audio/*"]
}
]
}
}
}

View File

@ -1,7 +0,0 @@
self.addEventListener('install', (e) => {
// Instalado
});
self.addEventListener('fetch', (e) => {
// Necessário para ser instalável
});