From 50baf78002dd7ff4ea52f68418e8638c866d8d92 Mon Sep 17 00:00:00 2001 From: IoannisMaras Date: Thu, 6 Aug 2026 23:13:41 +0300 Subject: [PATCH 1/2] fix(view): support range-mode pages for screenshot clipping --- src/officecli/CommandBuilder.View.cs | 13 ++- src/officecli/CommandBuilder.cs | 141 +++++++++++++++++++++++++-- src/officecli/Core/HtmlScreenshot.cs | 11 ++- src/officecli/ResidentServer.cs | 8 +- 4 files changed, 159 insertions(+), 14 deletions(-) diff --git a/src/officecli/CommandBuilder.View.cs b/src/officecli/CommandBuilder.View.cs index a3594e02c..54e8f1cfe 100644 --- a/src/officecli/CommandBuilder.View.cs +++ b/src/officecli/CommandBuilder.View.cs @@ -24,6 +24,7 @@ private static Command BuildViewCommand(Option jsonOption) var browserOpt = new Option("--browser") { Description = "Open output in browser (html / svg modes)" }; var outOpt = new Option("--out", "-o") { Description = "Output file path (html, screenshot, pdf modes; defaults to stdout for html, a temp file for screenshot)" }; var clipOpt = new Option("--range") { Description = "Restrict output to a region. Screenshot mode: an xlsx cell range ('Sheet1!A1:C3' or '/Sheet1/A1:C3') or any element data-path ('/slide[1]/shape[@id=N]', '/body/table[1]'); the PNG is cropped to the target's bounding box. Text mode (xlsx only): a cell range or single cell — emits just those rows/cells, saving context on large sheets. Not the character-offset `range=` prop of `set` (that one formats a text span like 3:7)." }; + var rangeModeOpt = new Option("--range-mode") { Description = "Screenshot range behavior: element (tight crop, default) or pages (complete containing page(s))", DefaultValueFactory = _ => "element" }; var screenshotWidthOpt = new Option("--screenshot-width") { Description = "Screenshot viewport width (default 1600)", DefaultValueFactory = _ => 1600 }; var screenshotHeightOpt = new Option("--screenshot-height") { Description = "Screenshot viewport height (default 1200)", DefaultValueFactory = _ => 1200 }; var gridOpt = new Option("--grid") @@ -47,6 +48,7 @@ private static Command BuildViewCommand(Option jsonOption) viewCommand.Add(browserOpt); viewCommand.Add(outOpt); viewCommand.Add(clipOpt); + viewCommand.Add(rangeModeOpt); viewCommand.Add(screenshotWidthOpt); viewCommand.Add(screenshotHeightOpt); viewCommand.Add(gridOpt); @@ -68,6 +70,13 @@ private static Command BuildViewCommand(Option jsonOption) var browser = result.GetValue(browserOpt); var outArg = result.GetValue(outOpt); var clipArg = result.GetValue(clipOpt); + var rangeMode = (result.GetValue(rangeModeOpt) ?? "element").ToLowerInvariant(); + if (rangeMode is not ("element" or "pages")) + throw new CliException($"Invalid --range-mode value: {rangeMode}. Valid: element, pages") + { Code = "invalid_value", ValidValues = ["element", "pages"] }; + if (rangeMode == "pages" && string.IsNullOrEmpty(clipArg)) + throw new CliException("--range-mode pages requires --range .") + { Code = "missing_argument" }; var screenshotWidth = result.GetValue(screenshotWidthOpt); var screenshotHeight = result.GetValue(screenshotHeightOpt); // --grid has three states: absent → off (0), present with no value @@ -128,6 +137,7 @@ private static Command BuildViewCommand(Option jsonOption) if (browser) req.Args["browser"] = "true"; if (outArg != null) req.Args["out"] = outArg; if (clipArg != null) req.Args["range"] = clipArg; + if (rangeMode != "element") req.Args["range-mode"] = rangeMode; req.Args["screenshot-width"] = screenshotWidth.ToString(); req.Args["screenshot-height"] = screenshotHeight.ToString(); if (gridCols != 0) req.Args["grid"] = gridCols.ToString(); // -1 = auto @@ -455,7 +465,8 @@ private static Command BuildViewCommand(Option jsonOption) File.WriteAllText(tmpHtml, html!); var r = clipArg != null ? OfficeCli.Core.HtmlScreenshot.CaptureClipped(tmpHtml, pngPath, - OfficeCli.Core.HtmlScreenshot.ResolveClipDataPaths(clipArg)) + OfficeCli.Core.HtmlScreenshot.ResolveClipDataPaths(clipArg), + containingPages: rangeMode == "pages") : OfficeCli.Core.HtmlScreenshot.Capture(tmpHtml, pngPath, screenshotWidth, screenshotHeight); try { File.Delete(tmpHtml); } catch { /* ignore */ } if (!r.Ok && r.Error == "clip_target_not_found") diff --git a/src/officecli/CommandBuilder.cs b/src/officecli/CommandBuilder.cs index 213e06703..8da1b02e0 100644 --- a/src/officecli/CommandBuilder.cs +++ b/src/officecli/CommandBuilder.cs @@ -1180,27 +1180,146 @@ handler is OfficeCli.Handlers.ExcelHandler case "view": { var mode = item.Mode ?? "text"; + var options = item.Options ?? new Dictionary(StringComparer.OrdinalIgnoreCase); + var allowedViewOptions = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "start", "end", "max-lines", "cols", "range", "type", "limit", + "page", "out", "render", "screenshot-width", "screenshot-height", "grid", "range-mode" + }; + var unknownOptions = options.Keys.Where(k => !allowedViewOptions.Contains(k)).ToList(); + if (unknownOptions.Count > 0) + throw new CliException($"Unknown view option(s): {string.Join(", ", unknownOptions)}") + { Code = "unknown_option" }; + + int? IntOption(string key) + { + if (!options.TryGetValue(key, out var value) || string.IsNullOrWhiteSpace(value)) return null; + if (int.TryParse(value, out var parsed)) return parsed; + throw new CliException($"view option '{key}' requires an integer; got '{value}'.") + { Code = "invalid_value" }; + } + var start = IntOption("start"); + var end = IntOption("end"); + var maxLines = IntOption("max-lines"); + var limit = IntOption("limit"); + var range = options.GetValueOrDefault("range"); + HashSet? cols = options.TryGetValue("cols", out var colsValue) + ? colsValue.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .ToHashSet(StringComparer.OrdinalIgnoreCase) + : null; if (mode.ToLowerInvariant() is "html" or "h") { if (handler is OfficeCli.Handlers.PowerPointHandler pptH) - return pptH.ViewAsHtml(); + { + var (pStart, pEnd) = ParsePptHtmlPage(options.GetValueOrDefault("page"), start, end, pptH); + return RenderViaRegistry(pptH, "pptx", new OfficeCli.Core.Rendering.RenderOptions + { StartPage = pStart, EndPage = pEnd })!; + } if (handler is OfficeCli.Handlers.ExcelHandler excelH) - return excelH.ViewAsHtml(); + return RenderViaRegistry(excelH, "xlsx", new OfficeCli.Core.Rendering.RenderOptions())!; if (handler is OfficeCli.Handlers.WordHandler wordH) - return wordH.ViewAsHtml(); + return RenderViaRegistry(wordH, "docx", new OfficeCli.Core.Rendering.RenderOptions + { PageFilter = options.GetValueOrDefault("page") })!; } if (mode.ToLowerInvariant() is "svg" or "g" && handler is OfficeCli.Handlers.PowerPointHandler pptSvg) { - return pptSvg.ViewAsSvg(1); + var page = IntOption("page") ?? 1; + return pptSvg.ViewAsSvg(page); + } + if (mode.ToLowerInvariant() is "screenshot" or "p") + { + var rangeMode = (options.GetValueOrDefault("range-mode") ?? "element").ToLowerInvariant(); + if (rangeMode is not ("element" or "pages")) + throw new CliException($"Invalid range-mode value: {rangeMode}. Valid: element, pages") + { Code = "invalid_value", ValidValues = ["element", "pages"] }; + if (rangeMode == "pages" && string.IsNullOrEmpty(range)) + throw new CliException("range-mode=pages requires options.range with an element path.") + { Code = "missing_argument" }; + var render = (options.GetValueOrDefault("render") ?? "auto").ToLowerInvariant(); + if (render is not ("auto" or "html" or "native")) + throw new CliException($"Invalid render value: {render}. Valid: auto, native, html") + { Code = "invalid_render", ValidValues = ["auto", "native", "html"] }; + // Batch renders the live in-memory DOM. A native Office + // renderer can only read the on-disk package and would + // therefore miss earlier unflushed items in this batch. + if (render == "native") + throw new CliException("Batch screenshots cannot use render=native because earlier batch edits are still in memory. Use render=html or auto.") + { Code = "native_requires_flush", Suggestion = "Use options.render=html (or auto), or run a standalone screenshot after batch --save." }; + + var sw = IntOption("screenshot-width") ?? 1600; + var sh = IntOption("screenshot-height") ?? 1200; + var pageFilter = options.GetValueOrDefault("page"); + var gridSpec = options.GetValueOrDefault("grid"); + var gridCols = gridSpec == null ? 0 : ParseGridSpec(gridSpec); + string html; + if (handler is OfficeCli.Handlers.PowerPointHandler pptShot) + { + var effectivePage = pageFilter; + if (range != null && string.IsNullOrEmpty(effectivePage) + && System.Text.RegularExpressions.Regex.Match(range, @"^/slide\[(\d+)\]") is { Success: true } sm) + effectivePage = sm.Groups[1].Value; + if (string.IsNullOrEmpty(effectivePage) && start is null && end is null && gridCols == 0) + effectivePage = "1"; + var (pStart, pEnd) = ParsePptHtmlPage(effectivePage, start, end, pptShot); + var (nativeW, nativeH) = pptShot.GetSlideNativePixels(); + var resolvedGrid = gridCols < 0 + ? OfficeCli.Core.HtmlScreenshot.AutoGridColumns( + (pEnd ?? pptShot.GetSlideCount()) - (pStart ?? 1) + 1, nativeW, nativeH) + : gridCols; + html = RenderViaRegistry(pptShot, "pptx", new OfficeCli.Core.Rendering.RenderOptions + { StartPage = pStart, EndPage = pEnd, GridColumns = resolvedGrid, ViewportPx = sw })!; + if (pStart == pEnd && resolvedGrid == 0) + { + if (sw == 1600 && sh == 1200) { sw = nativeW; sh = nativeH; } + else if (sh == 1200) sh = Math.Max(1, (int)Math.Round(sw * (double)nativeH / nativeW)); + } + } + else if (handler is OfficeCli.Handlers.ExcelHandler excelShot) + html = RenderViaRegistry(excelShot, "xlsx", new OfficeCli.Core.Rendering.RenderOptions())!; + else if (handler is OfficeCli.Handlers.WordHandler wordShot) + { + var effectivePage = range != null ? pageFilter : (string.IsNullOrEmpty(pageFilter) ? "1" : pageFilter); + html = RenderViaRegistry(wordShot, "docx", new OfficeCli.Core.Rendering.RenderOptions + { PageFilter = effectivePage })!; + if (int.TryParse(effectivePage, out _) && gridCols == 0) + { + var (nativeW, nativeH) = wordShot.GetPageNativePixels(); + if (sw == 1600 && sh == 1200) { sw = nativeW; sh = nativeH; } + else if (sh == 1200) sh = Math.Max(1, (int)Math.Round(sw * (double)nativeH / nativeW)); + } + } + else + throw new CliException("Screenshot mode is only supported for .pptx, .xlsx, and .docx files.") + { Code = "unsupported_type" }; + + var pngPath = options.GetValueOrDefault("out") + ?? Path.Combine(Path.GetTempPath(), $"officecli_screenshot_{Guid.NewGuid():N}.png"); + var tmpHtml = Path.Combine(Path.GetTempPath(), $"officecli_preview_{Guid.NewGuid():N}.html"); + try + { + File.WriteAllText(tmpHtml, html); + var capture = range != null + ? OfficeCli.Core.HtmlScreenshot.CaptureClipped(tmpHtml, pngPath, + OfficeCli.Core.HtmlScreenshot.ResolveClipDataPaths(range), + containingPages: rangeMode == "pages") + : OfficeCli.Core.HtmlScreenshot.Capture(tmpHtml, pngPath, sw, sh); + if (!capture.Ok) + throw new CliException("No headless browser available for batch screenshot." + + (capture.Error != null ? $" Last error: {capture.Error}" : "")) + { Code = "no_screenshot_backend" }; + } + finally { try { File.Delete(tmpHtml); } catch { } } + return Path.GetFullPath(pngPath); } return mode.ToLowerInvariant() switch { - "text" or "t" => handler.ViewAsText(null, null, null, null), - "annotated" or "a" => handler.ViewAsAnnotated(null, null, null, null), + "text" or "t" => handler.ViewAsText(start, end, maxLines, cols, range), + "annotated" or "a" => handler.ViewAsAnnotated(start, end, maxLines, cols), "outline" or "o" => handler.ViewAsOutline(), "stats" or "s" => handler.ViewAsStats(), - "issues" or "i" => OfficeCli.Core.OutputFormatter.FormatIssues(handler.ViewAsIssues(null, null), format), - _ => $"Unknown mode: {mode}" + "issues" or "i" => OfficeCli.Core.OutputFormatter.FormatIssues( + handler.ViewAsIssues(OfficeCli.Core.IssueSubtypes.Validate(options.GetValueOrDefault("type")), limit), format), + _ => throw new CliException($"Unknown view mode: {mode}") { Code = "invalid_value" } }; } case "raw": @@ -1237,7 +1356,11 @@ handler is OfficeCli.Handlers.ExcelHandler if (err.Path != null) lines.Add($" Path: {err.Path}"); if (err.Part != null) lines.Add($" Part: {err.Part}"); } - return string.Join("\n", lines); + // Validation is a judgment step, not a report-only read. A + // schema-invalid document must fail the item so an atomic + // edit+validate batch rolls its mutations back. + throw new CliException(string.Join("\n", lines)) + { Code = "validation_failed" }; } default: if (string.IsNullOrEmpty(item.Command)) diff --git a/src/officecli/Core/HtmlScreenshot.cs b/src/officecli/Core/HtmlScreenshot.cs index 787e77288..fe1cf926e 100644 --- a/src/officecli/Core/HtmlScreenshot.cs +++ b/src/officecli/Core/HtmlScreenshot.cs @@ -182,7 +182,7 @@ public static Result Capture(string htmlPath, string outPath, int width = 1600, /// a rendered element. /// public static Result CaptureClipped(string htmlPath, string outPath, IReadOnlyList dataPaths, - int padPx = 0, int scale = 2) + int padPx = 0, int scale = 2, bool containingPages = false) { if (FindChrome() == null) return new Result(false, "", "clip mode requires a Chrome-family browser (Chrome/Edge/Chromium)"); @@ -203,8 +203,13 @@ public static Result CaptureClipped(string htmlPath, string outPath, IReadOnlyLi "if(best)return best;if(cands.length===0)return null;" + "var el=cands[0];for(var an=el;an&&an!==document.documentElement;an=an.parentElement){" + "if(getComputedStyle(an).display==='none')an.style.display='block';}return el;}" + - "function _clipEls(){var els=[];_clipPaths.forEach(function(p){" + - "var el=_clipPick(p);if(el)els.push(el);});return els;}"; + (containingPages + ? "function _clipEls(){var els=[],seen=new Set();_clipPaths.forEach(function(p){" + + "var cands=document.querySelectorAll('[data-path=\"'+p+'\"]');cands.forEach(function(el){" + + "var page=el.closest('.page,.slide,.sheet-content')||el;" + + "if(!seen.has(page)){seen.add(page);els.push(page);}});});return els;}" + : "function _clipEls(){var els=[];_clipPaths.forEach(function(p){" + + "var el=_clipPick(p);if(el)els.push(el);});return els;}"); // The rect is reported via console.log -> --enable-logging=stderr in // the SAME chrome invocation that captures the full page: headless diff --git a/src/officecli/ResidentServer.cs b/src/officecli/ResidentServer.cs index 1659f0171..ddd9f7004 100644 --- a/src/officecli/ResidentServer.cs +++ b/src/officecli/ResidentServer.cs @@ -1559,6 +1559,11 @@ private void ExecuteView(ResidentRequest req, OutputFormat format) // --range clips a data-path region out of the HTML preview — mirrors // CommandBuilder.View.cs: native/direct-PNG backends are bypassed. var rangeArg = req.GetArgOrNull("range"); + var rangeMode = (req.GetArgOrNull("range-mode") ?? "element").ToLowerInvariant(); + if (rangeMode is not ("element" or "pages")) + throw new InvalidOperationException($"Invalid range-mode value: {rangeMode}. Valid: element, pages"); + if (rangeMode == "pages" && string.IsNullOrEmpty(rangeArg)) + throw new InvalidOperationException("range-mode=pages requires a range element path."); if (rangeArg != null) renderMode = "html"; var sw = req.GetIntArg("screenshot-width") ?? 1600; var sh = req.GetIntArg("screenshot-height") ?? 1200; @@ -1739,7 +1744,8 @@ private void ExecuteView(ResidentRequest req, OutputFormat format) File.WriteAllText(tmpHtml, html!); var rs = rangeArg != null ? OfficeCli.Core.HtmlScreenshot.CaptureClipped(tmpHtml, pngPath, - OfficeCli.Core.HtmlScreenshot.ResolveClipDataPaths(rangeArg)) + OfficeCli.Core.HtmlScreenshot.ResolveClipDataPaths(rangeArg), + containingPages: rangeMode == "pages") : OfficeCli.Core.HtmlScreenshot.Capture(tmpHtml, pngPath, sw, sh); try { File.Delete(tmpHtml); } catch { /* ignore */ } if (!rs.Ok) From adfc81d9234e7788eb7455c74fffa459b53f4ede Mon Sep 17 00:00:00 2001 From: IoannisMaras Date: Thu, 6 Aug 2026 23:22:00 +0300 Subject: [PATCH 2/2] fix(view): add range-mode pages support for clipped screenshots --- src/officecli/CommandBuilder.cs | 141 ++------------------------------ 1 file changed, 9 insertions(+), 132 deletions(-) diff --git a/src/officecli/CommandBuilder.cs b/src/officecli/CommandBuilder.cs index 8da1b02e0..213e06703 100644 --- a/src/officecli/CommandBuilder.cs +++ b/src/officecli/CommandBuilder.cs @@ -1180,146 +1180,27 @@ handler is OfficeCli.Handlers.ExcelHandler case "view": { var mode = item.Mode ?? "text"; - var options = item.Options ?? new Dictionary(StringComparer.OrdinalIgnoreCase); - var allowedViewOptions = new HashSet(StringComparer.OrdinalIgnoreCase) - { - "start", "end", "max-lines", "cols", "range", "type", "limit", - "page", "out", "render", "screenshot-width", "screenshot-height", "grid", "range-mode" - }; - var unknownOptions = options.Keys.Where(k => !allowedViewOptions.Contains(k)).ToList(); - if (unknownOptions.Count > 0) - throw new CliException($"Unknown view option(s): {string.Join(", ", unknownOptions)}") - { Code = "unknown_option" }; - - int? IntOption(string key) - { - if (!options.TryGetValue(key, out var value) || string.IsNullOrWhiteSpace(value)) return null; - if (int.TryParse(value, out var parsed)) return parsed; - throw new CliException($"view option '{key}' requires an integer; got '{value}'.") - { Code = "invalid_value" }; - } - var start = IntOption("start"); - var end = IntOption("end"); - var maxLines = IntOption("max-lines"); - var limit = IntOption("limit"); - var range = options.GetValueOrDefault("range"); - HashSet? cols = options.TryGetValue("cols", out var colsValue) - ? colsValue.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) - .ToHashSet(StringComparer.OrdinalIgnoreCase) - : null; if (mode.ToLowerInvariant() is "html" or "h") { if (handler is OfficeCli.Handlers.PowerPointHandler pptH) - { - var (pStart, pEnd) = ParsePptHtmlPage(options.GetValueOrDefault("page"), start, end, pptH); - return RenderViaRegistry(pptH, "pptx", new OfficeCli.Core.Rendering.RenderOptions - { StartPage = pStart, EndPage = pEnd })!; - } + return pptH.ViewAsHtml(); if (handler is OfficeCli.Handlers.ExcelHandler excelH) - return RenderViaRegistry(excelH, "xlsx", new OfficeCli.Core.Rendering.RenderOptions())!; + return excelH.ViewAsHtml(); if (handler is OfficeCli.Handlers.WordHandler wordH) - return RenderViaRegistry(wordH, "docx", new OfficeCli.Core.Rendering.RenderOptions - { PageFilter = options.GetValueOrDefault("page") })!; + return wordH.ViewAsHtml(); } if (mode.ToLowerInvariant() is "svg" or "g" && handler is OfficeCli.Handlers.PowerPointHandler pptSvg) { - var page = IntOption("page") ?? 1; - return pptSvg.ViewAsSvg(page); - } - if (mode.ToLowerInvariant() is "screenshot" or "p") - { - var rangeMode = (options.GetValueOrDefault("range-mode") ?? "element").ToLowerInvariant(); - if (rangeMode is not ("element" or "pages")) - throw new CliException($"Invalid range-mode value: {rangeMode}. Valid: element, pages") - { Code = "invalid_value", ValidValues = ["element", "pages"] }; - if (rangeMode == "pages" && string.IsNullOrEmpty(range)) - throw new CliException("range-mode=pages requires options.range with an element path.") - { Code = "missing_argument" }; - var render = (options.GetValueOrDefault("render") ?? "auto").ToLowerInvariant(); - if (render is not ("auto" or "html" or "native")) - throw new CliException($"Invalid render value: {render}. Valid: auto, native, html") - { Code = "invalid_render", ValidValues = ["auto", "native", "html"] }; - // Batch renders the live in-memory DOM. A native Office - // renderer can only read the on-disk package and would - // therefore miss earlier unflushed items in this batch. - if (render == "native") - throw new CliException("Batch screenshots cannot use render=native because earlier batch edits are still in memory. Use render=html or auto.") - { Code = "native_requires_flush", Suggestion = "Use options.render=html (or auto), or run a standalone screenshot after batch --save." }; - - var sw = IntOption("screenshot-width") ?? 1600; - var sh = IntOption("screenshot-height") ?? 1200; - var pageFilter = options.GetValueOrDefault("page"); - var gridSpec = options.GetValueOrDefault("grid"); - var gridCols = gridSpec == null ? 0 : ParseGridSpec(gridSpec); - string html; - if (handler is OfficeCli.Handlers.PowerPointHandler pptShot) - { - var effectivePage = pageFilter; - if (range != null && string.IsNullOrEmpty(effectivePage) - && System.Text.RegularExpressions.Regex.Match(range, @"^/slide\[(\d+)\]") is { Success: true } sm) - effectivePage = sm.Groups[1].Value; - if (string.IsNullOrEmpty(effectivePage) && start is null && end is null && gridCols == 0) - effectivePage = "1"; - var (pStart, pEnd) = ParsePptHtmlPage(effectivePage, start, end, pptShot); - var (nativeW, nativeH) = pptShot.GetSlideNativePixels(); - var resolvedGrid = gridCols < 0 - ? OfficeCli.Core.HtmlScreenshot.AutoGridColumns( - (pEnd ?? pptShot.GetSlideCount()) - (pStart ?? 1) + 1, nativeW, nativeH) - : gridCols; - html = RenderViaRegistry(pptShot, "pptx", new OfficeCli.Core.Rendering.RenderOptions - { StartPage = pStart, EndPage = pEnd, GridColumns = resolvedGrid, ViewportPx = sw })!; - if (pStart == pEnd && resolvedGrid == 0) - { - if (sw == 1600 && sh == 1200) { sw = nativeW; sh = nativeH; } - else if (sh == 1200) sh = Math.Max(1, (int)Math.Round(sw * (double)nativeH / nativeW)); - } - } - else if (handler is OfficeCli.Handlers.ExcelHandler excelShot) - html = RenderViaRegistry(excelShot, "xlsx", new OfficeCli.Core.Rendering.RenderOptions())!; - else if (handler is OfficeCli.Handlers.WordHandler wordShot) - { - var effectivePage = range != null ? pageFilter : (string.IsNullOrEmpty(pageFilter) ? "1" : pageFilter); - html = RenderViaRegistry(wordShot, "docx", new OfficeCli.Core.Rendering.RenderOptions - { PageFilter = effectivePage })!; - if (int.TryParse(effectivePage, out _) && gridCols == 0) - { - var (nativeW, nativeH) = wordShot.GetPageNativePixels(); - if (sw == 1600 && sh == 1200) { sw = nativeW; sh = nativeH; } - else if (sh == 1200) sh = Math.Max(1, (int)Math.Round(sw * (double)nativeH / nativeW)); - } - } - else - throw new CliException("Screenshot mode is only supported for .pptx, .xlsx, and .docx files.") - { Code = "unsupported_type" }; - - var pngPath = options.GetValueOrDefault("out") - ?? Path.Combine(Path.GetTempPath(), $"officecli_screenshot_{Guid.NewGuid():N}.png"); - var tmpHtml = Path.Combine(Path.GetTempPath(), $"officecli_preview_{Guid.NewGuid():N}.html"); - try - { - File.WriteAllText(tmpHtml, html); - var capture = range != null - ? OfficeCli.Core.HtmlScreenshot.CaptureClipped(tmpHtml, pngPath, - OfficeCli.Core.HtmlScreenshot.ResolveClipDataPaths(range), - containingPages: rangeMode == "pages") - : OfficeCli.Core.HtmlScreenshot.Capture(tmpHtml, pngPath, sw, sh); - if (!capture.Ok) - throw new CliException("No headless browser available for batch screenshot." - + (capture.Error != null ? $" Last error: {capture.Error}" : "")) - { Code = "no_screenshot_backend" }; - } - finally { try { File.Delete(tmpHtml); } catch { } } - return Path.GetFullPath(pngPath); + return pptSvg.ViewAsSvg(1); } return mode.ToLowerInvariant() switch { - "text" or "t" => handler.ViewAsText(start, end, maxLines, cols, range), - "annotated" or "a" => handler.ViewAsAnnotated(start, end, maxLines, cols), + "text" or "t" => handler.ViewAsText(null, null, null, null), + "annotated" or "a" => handler.ViewAsAnnotated(null, null, null, null), "outline" or "o" => handler.ViewAsOutline(), "stats" or "s" => handler.ViewAsStats(), - "issues" or "i" => OfficeCli.Core.OutputFormatter.FormatIssues( - handler.ViewAsIssues(OfficeCli.Core.IssueSubtypes.Validate(options.GetValueOrDefault("type")), limit), format), - _ => throw new CliException($"Unknown view mode: {mode}") { Code = "invalid_value" } + "issues" or "i" => OfficeCli.Core.OutputFormatter.FormatIssues(handler.ViewAsIssues(null, null), format), + _ => $"Unknown mode: {mode}" }; } case "raw": @@ -1356,11 +1237,7 @@ handler is OfficeCli.Handlers.ExcelHandler if (err.Path != null) lines.Add($" Path: {err.Path}"); if (err.Part != null) lines.Add($" Part: {err.Part}"); } - // Validation is a judgment step, not a report-only read. A - // schema-invalid document must fail the item so an atomic - // edit+validate batch rolls its mutations back. - throw new CliException(string.Join("\n", lines)) - { Code = "validation_failed" }; + return string.Join("\n", lines); } default: if (string.IsNullOrEmpty(item.Command))